Перейти до основного вмісту

Error Handling в Node.js

Error Handling у Node.js vs Java

У Java ми звикли до checked та unchecked exceptions, блоків try-catch-finally, та Spring'івських @ExceptionHandler. JavaScript має інший підхід до обробки помилок, особливо в асинхронному коді.

// Java - знайомий підхід
try {
    User user = userService.getUser(id);
    processUser(user);
} catch (UserNotFoundException e) {
    logger.error("User not found", e);
} catch (Exception e) {
    logger.error("Unexpected error", e);
} finally {
    cleanup();
}

// JavaScript/TypeScript - схожий синтаксис, різна семантика
try {
    const user = await userService.getUser(id);
    await processUser(user);
} catch (err) {
    if (err instanceof UserNotFoundError) {
        logger.error('User not found', err);
    } else {
        logger.error('Unexpected error', err);
    }
} finally {
    cleanup();
}

Ключова відмінність: У JavaScript немає checked exceptions. Всі помилки unchecked, компілятор не змушує їх обробляти.

1. Синхронний Error Handling: try/catch

Для синхронного коду try/catch працює ідентично до Java:

function parseUserData(jsonString: string): User {
    try {
        const data = JSON.parse(jsonString);
        
        if (!data.id || !data.email) {
            throw new ValidationError('Missing required fields');
        }
        
        return {
            id: data.id,
            email: data.email,
            name: data.name || 'Unknown'
        };
    } catch (err) {
        if (err instanceof SyntaxError) {
            console.error('Invalid JSON:', err.message);
            throw new ValidationError('Invalid JSON format');
        }
        if (err instanceof ValidationError) {
            throw err; // Прокидаємо далі
        }
        // Unexpected error
        console.error('Unexpected error:', err);
        throw err;
    }
}

Створення власних класів помилок

// Базовий клас для доменних помилок
class AppError extends Error {
    constructor(
        public message: string,
        public statusCode: number = 500,
        public isOperational: boolean = true
    ) {
        super(message);
        this.name = this.constructor.name;
        Error.captureStackTrace(this, this.constructor);
    }
}

// Конкретні типи помилок (як у Java ієрархія Exception)
class ValidationError extends AppError {
    constructor(message: string) {
        super(message, 400);
    }
}

class NotFoundError extends AppError {
    constructor(resource: string) {
        super(`${resource} not found`, 404);
    }
}

class UnauthorizedError extends AppError {
    constructor(message: string = 'Unauthorized') {
        super(message, 401);
    }
}

class DatabaseError extends AppError {
    constructor(message: string) {
        super(message, 500, false); // не операційна помилка
    }
}

// Використання
function getUser(id: number): User {
    const user = database.find(id);
    if (!user) {
        throw new NotFoundError('User');
    }
    return user;
}

2. Асинхронний Error Handling: Promises

Критично важливо: try/catch НЕ ловить помилки в Promise без await!

// ❌ ЦЕ НЕ СПРАЦЮЄ!
function brokenErrorHandling() {
    try {
        fetchUser(123).then(user => {
            console.log(user);
        });
    } catch (err) {
        // Цей catch НІКОЛИ не спрацює для помилок у Promise!
        console.error(err);
    }
}

// ✅ ПРАВИЛЬНО: використовуємо .catch()
function correctErrorHandling() {
    fetchUser(123)
        .then(user => {
            console.log(user);
        })
        .catch(err => {
            // Тут ловимо помилки з Promise
            console.error('Error:', err);
        });
}

// ✅ АБО з async/await
async function asyncErrorHandling() {
    try {
        const user = await fetchUser(123);
        console.log(user);
    } catch (err) {
        // З await try/catch працює!
        console.error('Error:', err);
    }
}

Promise Rejection Handling

// Створення Promise з можливістю rejection
function fetchUserFromAPI(id: number): Promise<User> {
    return new Promise((resolve, reject) => {
        http.get(`/api/users/${id}`, (res) => {
            if (res.statusCode === 404) {
                // Відхиляємо Promise
                reject(new NotFoundError('User'));
                return;
            }
            
            if (res.statusCode !== 200) {
                reject(new Error(`HTTP ${res.statusCode}`));
                return;
            }
            
            let data = '';
            res.on('data', chunk => data += chunk);
            res.on('end', () => {
                try {
                    const user = JSON.parse(data);
                    resolve(user); // Успішне виконання
                } catch (err) {
                    reject(err); // JSON parse error
                }
            });
        }).on('error', err => {
            reject(err); // Network error
        });
    });
}

// Використання
fetchUserFromAPI(123)
    .then(user => console.log('Success:', user))
    .catch(err => {
        if (err instanceof NotFoundError) {
            console.log('User not found');
        } else {
            console.error('Error:', err);
        }
    });

Promise Chain Error Propagation

// Помилка в будь-якому .then() автоматично пробрасується до .catch()
fetchUser(123)
    .then(user => {
        console.log('Step 1: User fetched');
        return fetchOrders(user.id);
    })
    .then(orders => {
        console.log('Step 2: Orders fetched');
        if (orders.length === 0) {
            throw new Error('No orders found'); // Створюємо помилку
        }
        return processOrders(orders);
    })
    .then(result => {
        console.log('Step 3: Orders processed');
        return result;
    })
    .catch(err => {
        // Ловимо помилки з БУДЬ-ЯКОГО .then() вище
        console.error('Error in chain:', err);
        // Можемо повернути fallback значення
        return { orders: [], status: 'error' };
    })
    .finally(() => {
        // Виконується завжди (як Java finally)
        console.log('Cleanup');
    });

3. Async/Await Error Handling

З async/await обробка помилок виглядає як у синхронному Java-коді:

async function processUserOrder(userId: number, orderId: number) {
    try {
        // Кожен await може кинути помилку
        const user = await fetchUser(userId);
        console.log('User found:', user.email);
        
        const order = await fetchOrder(orderId);
        console.log('Order found:', order.id);
        
        // Валідація
        if (order.userId !== user.id) {
            throw new UnauthorizedError('Order does not belong to user');
        }
        
        const payment = await processPayment(order);
        console.log('Payment processed:', payment.id);
        
        await sendConfirmationEmail(user.email, order);
        console.log('Email sent');
        
        return { success: true, orderId: order.id };
        
    } catch (err) {
        // Обробка різних типів помилок
        if (err instanceof NotFoundError) {
            console.error('Resource not found:', err.message);
            return { success: false, error: 'NOT_FOUND' };
        }
        
        if (err instanceof UnauthorizedError) {
            console.error('Unauthorized:', err.message);
            return { success: false, error: 'UNAUTHORIZED' };
        }
        
        if (err instanceof ValidationError) {
            console.error('Validation error:', err.message);
            return { success: false, error: 'VALIDATION_ERROR' };
        }
        
        // Непередбачена помилка
        console.error('Unexpected error:', err);
        throw err; // Прокидаємо далі
    } finally {
        // Очищення ресурсів (як у Java)
        console.log('Cleaning up...');
    }
}

Паралельні операції та error handling

// Promise.all - fail-fast (якщо одна помилка, все падає)
async function loadDashboardData(userId: number) {
    try {
        const [user, orders, notifications, settings] = await Promise.all([
            fetchUser(userId),
            fetchOrders(userId),
            fetchNotifications(userId),
            fetchSettings(userId)
        ]);
        
        return { user, orders, notifications, settings };
    } catch (err) {
        // Якщо хоча б один запит failed
        console.error('Failed to load dashboard:', err);
        throw err;
    }
}

// Promise.allSettled - чекаємо на всі, обробляємо окремо
async function loadDashboardDataResilient(userId: number) {
    const results = await Promise.allSettled([
        fetchUser(userId),
        fetchOrders(userId),
        fetchNotifications(userId),
        fetchSettings(userId)
    ]);
    
    const [userResult, ordersResult, notifResult, settingsResult] = results;
    
    return {
        user: userResult.status === 'fulfilled' ? userResult.value : null,
        orders: ordersResult.status === 'fulfilled' ? ordersResult.value : [],
        notifications: notifResult.status === 'fulfilled' ? notifResult.value : [],
        settings: settingsResult.status === 'fulfilled' ? settingsResult.value : getDefaultSettings(),
        errors: results
            .filter(r => r.status === 'rejected')
            .map(r => (r as PromiseRejectedResult).reason)
    };
}

4. Global Error Handlers: unhandledRejection та uncaughtException

Це еквівалент Java UncaughtExceptionHandler. Використовуються як safety net для помилок, які не були оброблені.

// Необроблені Promise rejections (КРИТИЧНО ВАЖЛИВО!)
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
    console.error('Unhandled Rejection at:', promise);
    console.error('Reason:', reason);
    
    // Логування в production
    logger.error('Unhandled Promise Rejection', {
        reason: reason instanceof Error ? reason.message : reason,
        stack: reason instanceof Error ? reason.stack : undefined,
        promise: promise.toString()
    });
    
    // В production часто роблять graceful shutdown
    // process.exit(1);
});

// Необроблені синхронні exception
process.on('uncaughtException', (err: Error) => {
    console.error('Uncaught Exception:', err);
    logger.error('Uncaught Exception', { 
        message: err.message, 
        stack: err.stack 
    });
    
    // ОБОВ'ЯЗКОВО завершити процес після uncaughtException
    // Стан програми може бути непередбачуваним
    process.exit(1);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
    console.log('SIGTERM received, shutting down gracefully');
    
    // Закриваємо сервер
    server.close(async () => {
        console.log('HTTP server closed');
        
        // Закриваємо з'єднання з БД
        await database.close();
        console.log('Database connection closed');
        
        process.exit(0);
    });
    
    // Якщо не закрився за 10 секунд - форсуємо
    setTimeout(() => {
        console.error('Forced shutdown');
        process.exit(1);
    }, 10000);
});

Приклад: Помилка без обробки

// ❌ НЕБЕЗПЕЧНО: Promise rejection без .catch()
async function dangerousCode() {
    // Якщо fetchUser кине помилку, вона не буде оброблена
    fetchUser(123); // Забули await і .catch()!
}

dangerousCode();

// Node.js виведе:
// (node:12345) UnhandledPromiseRejectionWarning: Error: User not found
// (node:12345) UnhandledPromiseRejectionWarning: Unhandled promise rejection...

// ✅ ПРАВИЛЬНО
async function safeCode() {
    try {
        await fetchUser(123);
    } catch (err) {
        console.error('Handled error:', err);
    }
}

// АБО
function alsoSafeCode() {
    fetchUser(123).catch(err => {
        console.error('Handled error:', err);
    });
}

5. Express.js Error Handling

Express має спеціальний middleware для обробки помилок (аналог Spring @ExceptionHandler):

import express, { Request, Response, NextFunction } from 'express';

const app = express();

// Звичайні route handlers
app.get('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const user = await userService.getUser(parseInt(req.params.id));
        res.json(user);
    } catch (err) {
        // Передаємо помилку в error handling middleware
        next(err);
    }
});

// Error handling middleware (завжди 4 параметри!)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    console.error('Error:', err);
    
    // Обробка різних типів помилок
    if (err instanceof ValidationError) {
        return res.status(400).json({
            error: 'Validation Error',
            message: err.message
        });
    }
    
    if (err instanceof NotFoundError) {
        return res.status(404).json({
            error: 'Not Found',
            message: err.message
        });
    }
    
    if (err instanceof UnauthorizedError) {
        return res.status(401).json({
            error: 'Unauthorized',
            message: err.message
        });
    }
    
    // Загальна помилка сервера
    res.status(500).json({
        error: 'Internal Server Error',
        message: process.env.NODE_ENV === 'production' 
            ? 'Something went wrong' 
            : err.message
    });
});

Async Error Handler Wrapper

// Utility для автоматичної обробки async помилок
function asyncHandler(fn: Function) {
    return (req: Request, res: Response, next: NextFunction) => {
        Promise.resolve(fn(req, res, next)).catch(next);
    };
}

// Використання (без try-catch в кожному handler!)
app.get('/users/:id', asyncHandler(async (req: Request, res: Response) => {
    const user = await userService.getUser(parseInt(req.params.id));
    res.json(user);
}));

// Якщо getUser() кине помилку, вона автоматично потрапить в error middleware

// Або використовуйте готову бібліотеку:
// npm install express-async-errors
import 'express-async-errors'; // Просто імпортуємо один раз
// Тепер всі async помилки автоматично обробляються!

6. Best Practices для Error Handling

Правило Пояснення Приклад
Завжди обробляйте Promise rejections Кожен Promise повинен мати .catch() або бути в try-catch await fetchData().catch(err => ...)
Використовуйте власні Error класи Замість throw new Error() створюйте типізовані помилки throw new NotFoundError('User')
Не ігноруйте помилки Порожній catch - антипаттерн ❌ .catch(() => {})
Логуйте контекст Додавайте корисну інформацію при логуванні logger.error('Failed', {userId, orderId})
Розрізняйте operational vs programmer errors Operational можна обробити, programmer - ні NotFoundError (operational) vs TypeError (programmer)

Operational vs Programmer Errors

// Operational errors (очікувані, можна обробити)
class OperationalError extends Error {
    isOperational = true;
}

// Приклади operational errors:
// - NotFoundError (користувач не знайдений)
// - ValidationError (невалідні дані)
// - UnauthorizedError (не авторизований)
// - NetworkError (мережа недоступна)

// Programmer errors (баги в коді, НЕ можна обробити)
// - TypeError (undefined is not a function)
// - ReferenceError (variable is not defined)
// - Logic errors (неправильна бізнес-логіка)

function errorHandler(err: Error) {
    if (err instanceof OperationalError) {
        // Можемо обробити і продовжити роботу
        logger.error('Operational error:', err);
        sendErrorResponse(err);
    } else {
        // Programmer error - треба фіксити код
        logger.fatal('Programmer error - shutting down:', err);
        process.exit(1);
    }
}

Централізована обробка помилок

// errorHandler.ts - центральний обробник (як Spring @ControllerAdvice)
class ErrorHandler {
    private logger: Logger;
    
    constructor(logger: Logger) {
        this.logger = logger;
    }
    
    handleError(err: Error): void {
        this.logger.error('Error occurred:', {
            name: err.name,
            message: err.message,
            stack: err.stack
        });
        
        if (!this.isOperationalError(err)) {
            // Programmer error - треба перезапустити
            process.exit(1);
        }
    }
    
    isOperationalError(err: Error): boolean {
        if (err instanceof AppError) {
            return err.isOperational;
        }
        return false;
    }
    
    setupGlobalHandlers(): void {
        process.on('unhandledRejection', (reason: any) => {
            throw reason; // Перетворюємо в uncaughtException
        });
        
        process.on('uncaughtException', (err: Error) => {
            this.handleError(err);
            
            if (!this.isOperationalError(err)) {
                process.exit(1);
            }
        });
    }
}

// Використання
const errorHandler = new ErrorHandler(logger);
errorHandler.setupGlobalHandlers();

7. Retry та Circuit Breaker Patterns

// Retry pattern з exponential backoff
async function fetchWithRetry<T>(
    fn: () => Promise<T>,
    maxRetries: number = 3,
    baseDelay: number = 1000
): Promise<T> {
    let lastError: Error;
    
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            return await fn();
        } catch (err) {
            lastError = err as Error;
            
            // Не retry на деяких помилках
            if (err instanceof ValidationError || err instanceof UnauthorizedError) {
                throw err;
            }
            
            if (attempt < maxRetries - 1) {
                const delay = baseDelay * Math.pow(2, attempt); // Exponential backoff
                console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }
    
    throw lastError!;
}

// Використання
const user = await fetchWithRetry(() => fetchUser(123), 3, 1000);

// Circuit Breaker pattern (спрощена версія)
class CircuitBreaker {
    private failures: number = 0;
    private lastFailTime: number = 0;
    private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
    
    constructor(
        private threshold: number = 5,
        private timeout: number = 60000 // 1 хвилина
    ) {}
    
    async execute<T>(fn: () => Promise<T>): Promise<T> {
        if (this.state === 'OPEN') {
            if (Date.now() - this.lastFailTime > this.timeout) {
                this.state = 'HALF_OPEN';
            } else {
                throw new Error('Circuit breaker is OPEN');
            }
        }
        
        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (err) {
            this.onFailure();
            throw err;
        }
    }
    
    private onSuccess(): void {
        this.failures = 0;
        this.state = 'CLOSED';
    }
    
    private onFailure(): void {
        this.failures++;
        this.lastFailTime = Date.now();
        
        if (this.failures >= this.threshold) {
            this.state = 'OPEN';
            console.error('Circuit breaker opened!');
        }
    }
}

// Використання
const breaker = new CircuitBreaker(5, 60000);
const user = await breaker.execute(() => fetchUser(123));

8. Логування помилок у Production

// Структуроване логування (використовуйте winston, pino, або bunyan)
import winston from 'winston';

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    defaultMeta: { service: 'user-service' },
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
    ]
});

// Middleware для логування всіх запитів
app.use((req: Request, res: Response, next: NextFunction) => {
    const start = Date.now();
    
    res.on('finish', () => {
        const duration = Date.now() - start;
        logger.info('HTTP Request', {
            method: req.method,
            url: req.url,
            status: res.statusCode,
            duration,
            userAgent: req.get('user-agent')
        });
    });
    
    next();
});

// Error logging middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    logger.error('Request failed', {
        error: {
            name: err.name,
            message: err.message,
            stack: err.stack
        },
        request: {
            method: req.method,
            url: req.url,
            headers: req.headers,
            body: req.body
        }
    });
    
    next(err);
});

Чеклист для Error Handling

  • ✅ Всі Promise мають .catch() або обгорнуті в try-catch
  • ✅ Встановлено global handlers: unhandledRejection, uncaughtException
  • ✅ Використовуються власні Error класи для різних типів помилок
  • ✅ Express має error handling middleware
  • ✅ Розрізняються operational та programmer errors
  • ✅ Логування включає контекст (userId, requestId тощо)
  • ✅ В production не показуються stack traces користувачам
  • ✅ Реалізовано graceful shutdown
  • ✅ Критичні операції мають retry logic
  • ✅ Зовнішні сервіси захищені circuit breaker

Коментарі

Популярні публікації

Створення нового Elixir-проєкту

Для створення новго Elixir-проєкту можна використати команду mix new first_project --sup Зрозуміло, що Elixir має бути встановлений раніше. Пояснення команди: mix — це вбудований інструмент для управління проєктами в Elixir (аналог maven у Java чи npm у JavaScript ). new — підкоманда mix, яка створює новий проєкт. first_project — назва твого нового проєкту. Папка з цією назвою буде створена у поточному каталозі. --sup — опціональний прапорець, який додає шаблон структури з Supervision Tree. Це означає, що створений проєкт одразу буде мати структуру, яка підтримує супервізор (супервізор керує життєвим циклом процесів у системі, перезапускаючи їх при падінні). Щоб створити файл з тестом, можна запустити команду із директорії проєкту mix test Приблизний вигляд структури проєкту:

Агрегати в DDD

Domain-Driven Design (DDD, предметно-орієнтоване проєктування) — це підхід до розробки програмного забезпечення, який зосереджується на моделюванні бізнес-логіки на основі реального домену (предметної області). Його запропонував Ерік Еванс у своїй книзі "Domain-Driven Design: Tackling Complexity in the Heart of Software". Основні принципи DDD Фокус на домені – головна увага приділяється предметній області, а не технічним деталям. Єдина мова (Ubiquitous Language) – розробники, бізнес-аналітики та інші учасники проєкту використовують спільну термінологію, щоб уникнути непорозумінь. Бізнес-логіка відокремлена від технічної реалізації – код моделюється так, щоб він чітко відображав реальний бізнес-процес. Основні концепції DDD Entity (Сутність) – об’єкт з унікальним ідентифікатором, що зберігається в системі (наприклад, Користувач, Замовлення). Value Object (Об’єкт-значення) – об’єкт, який не має унікального ідентифікатора та є незмінним (наприклад, Адреса або Гроші)...

Стратегії ребалансування в Kafka

Стратегії ребалансування в Kafka Ребалансування (Rebalancing) — це процес перерозподілу партицій між споживачами (сonsumer) у групі (Consumer Group). Kafka має кілька стратегій ребалансування: RangeAssignor. Ця стратегія розподіляє партиції на основі діапазонів, які створюються відповідно до сортування топіків і партицій. Наприклад, якщо є два консюмери і 6 партицій (P0–P5), перший консюмер отримає P0–P2, а другий — P3–P5. Особливості: Простий алгоритм. Може призводити до нерівномірного розподілу, якщо кількість партицій не ділиться порівну між консюмерами. RoundRobinAssignor. Ця стратегія рівномірно розподіляє партиції між консюмерами за круговим принципом. Наприклад, якщо є два консюмери і 6 партицій, перший отримає P0, P2, P4, а другий — P1, P3, P5. Особливості: Гарантує більш рівномірний розподіл партицій. Використовується в багатотопікових сценаріях. StickyAssignor. Ця стратегія намагається мінімізувати кількість змін у розподілі партицій між консюмерами при ре...

Основи Elixir

Elixir — це функційна мова програмування, яка працює на віртуальній машині Erlang (BEAM). Вона призначена для створення масштабованих і відмовостійких систем. Elixir успадкував багато переваг Erlang, таких як легкість паралельного програмування та висока доступність, але також додав сучасний синтаксис та інструменти для розробки. Основні концепції Elixir Elixir є функційною мовою, тому вона орієнтована на використання функцій та незмінних даних. Ось декілька ключових концепцій: Незмінність даних. Усі дані в Elixir є незмінними, що спрощує роботу з паралельними процесами. Функції. Функції є основним будівельним блоком програми. Вони можуть бути анонімними або іменованими. Паттерн-матчинг. Elixir використовує паттерн-матчинг для роботи з даними, що дозволяє легко розбирати структури даних. Процеси. Elixir використовує легкі процеси для паралельного виконання завдань. Ці процеси ізольовані та спілкуються через передачу повідомлень. Синтаксис Elixir Синтаксис Elixir є прос...

Angular CLI

CLI (command-line interface) – інтерфейс командного рядка. Перед початком роботи має бути встановлений Node.js Встановлення: npm install -g @angular/cli Отримання допомоги: ng help Буде приблизно такий результат: add Adds support for an external library to your project. analytics Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering. build (b) Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory. deploy Invokes the deploy builder for a specified project or for the default project in the workspace. config Retrieves or sets Angular configuration values in the angular.json file for the workspace. doc (d) Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword. e2e (e) Builds and serves an Angular app, then runs end-to-end tests. extract-i18n (i18n-extract, xi18n) Extracts i18n mes...