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

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

Коментарі

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

Шпаргалка по базових командах PostgreSQL

1. Підключення до PostgreSQL через командний рядок: psql -h <host> -p <port> -U <username> -d <database> 2. Підключення до бази без параметрів (якщо користувач і база мають однакове ім’я): psql 3. Показати список усіх баз даних: \l 4. Підключитися до іншої бази даних: \c <database_name> 5. Показати список таблиць у поточній базі: \dt 6. Показати всі об'єкти (таблиці, індекси, секвенції): \d 7. Показати таблиці з усіх схем: \dt *.* 8. Переглянути структуру конкретної таблиці: \d <table_name> 9. Виконати SQL-запит (приклад): SELECT * FROM users; 10. Вийти з psql: \q 11. Створити нову базу даних: CREATE DATABASE mydb; 12. Створити нову таблицю: CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE ); 13. Додати новий запис: INSERT INTO users (name, email) VALUES ('Іван', 'ivan@example.com'); 14. Оновити дані в таблиці: UPDATE users SET name = 'Петро' WH...

Встановлення Visual Studio Code на Ubuntu

Visual Studio Code (VS Code) — це потужний, легкий та кросплатформний редактор коду, який став стандартом для розробників завдяки універсальності та швидкості роботи. Він підтримує Windows, Linux і macOS, запускається швидше за повноцінні IDE, але водночас може перетворюватися на повнофункціональне середовище розробки завдяки тисячам розширень. VS Code має продуману архітектуру, інтуїтивний інтерфейс, вбудований термінал та інтелектуальну систему автодоповнення на базі Language Server Protocol, що забезпечує високоякісну підтримку синтаксису для багатьох мов програмування. Серед ключових можливостей — розумне автодоповнення коду (IntelliSense), навігація між класами, символами та файлами, рефакторинг, форматування коду, інтеграція з Git, підтримка віддаленої розробки через SSH або контейнерні середовища (Dev Containers). Завдяки відлагоджувачу (debugger), який налаштовується під будь-яку мову, розробник може запускати код у контрольованому середовищі, ставити брейкпоїнти, переглядат...

Керуючі конструкції в Elixir

Elixir пропонує різноманітні керуючі конструкції для управління потоком виконання програми. На відміну від імперативних мов, всі конструкції в Elixir повертають значення, що робить код більш виразним. if та else Конструкція if перевіряє умову і виконує код відповідно до результату: # Базовий синтаксис if true do "Це буде виконано" else "Це не буде виконано" end # Однорядковий варіант if connected?, do: "Підключено", else: "Відключено" # Присвоєння результату message = if age >= 18 do "Ви повнолітній" else "Ви неповнолітній" end # Без else (повертає nil) if user_logged_in? do show_dashboard() end Примітка: В Elixir тільки false і nil вважаються хибними. Всі інші значення (включаючи 0, "", []) вважаються істинними. unless Конструкція unless — це протилежність if, виконується коли умова хибна: # ...

Дамп бази даних Postgresql локально

Що потрібно встановити локально (Ubuntu) Встанови клієнтські утиліти PostgreSQL: pg_dump , psql , pg_restore . sudo apt update sudo apt install postgresql-client Кроки міграції Ідея: не зберігати дамп на серверах. Зробити дамп з віддаленого сервера на свій локальний ПК, потім передати його на новий сервер і там відновити. 1) Створення дампу з віддаленого сервера локально pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB -f dump_file.dump Пояснення ключів: -h (хост), -U (користувач), -Fc (custom format), -d (БД), -f (вихідний файл локально). 1.1) Варіант з розбиттям на частини (якщо треба) pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB | split -b 10G - dump_file.dump.part_ Це створить локально файли типу dump_file.dump.part_aa , dump_file.dump.part_ab тощо. 2) Передача дампу на новий сервер Один файл: scp dump_file.dump NEW_USER@NEW_SERVER_IP:/tmp/ Або частинами: scp dump_file.dump.part_* NEW_USER@NEW_SERVER_IP:/tmp/ 3) Збирання файл...

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...