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

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

Основи GLSL

Що таке GLSL? GLSL (OpenGL Shading Language) – мова програмування шейдерів для OpenGL. Використовується для написання vertex, fragment, geometry та інших шейдерів, що працюють на GPU. Оголошення версії #version 330 core Вказує версію GLSL. Наприклад, 330 core відповідає OpenGL 3.3. Вхідні та вихідні змінні layout(location=0) in vec3 aPos; layout(location=1) in vec3 aNormal; out vec3 FragPos; in – вхідні атрибути (vertex shader). out – вихідні змінні (vertex shader) або фінальний колір (fragment shader). Основні типи даних float, int, bool vec2, vec3, vec4 mat2, mat3, mat4 sampler2D (текстури) Тип Опис Приклади використання vec2 Двокомпонентний вектор з типом float. - Текстурні координати (UV) - 2D позиції - Швидкість у 2D vec3 Трикомпонентний вектор з типом float. - Координати позицій у 3D - Нормалі - Колір у форматі RGB vec4 Чотирикомпо...

Атоми в мові програмування Elixir

Атоми в Elixir Атоми є фундаментальною концепцією в Elixir , що відіграє ключову роль у створенні надійних та масштабованих систем. В Elixir це специфічний тип даних, який є константою , незмінною , ідентифікованою за своїм ім'ям . Отже, атом в Elixir — це іменована константа, що представляє себе. Уявіть, що ви даєте унікальне ім'я певній речі, і це ім'я завжди посилається саме на цю річ, і ніколи на щось інше. Наприклад, атом :ok завжди буде означати саме успішне завершення операції, а не якесь інше значення. Технічно, атоми є похідними від чисел . Кожен унікальний атом зберігається у таблиці атомів, і йому присвоюється унікальний цілочисельний ідентифікатор. Це робить їх надзвичайно ефективними для порівняння: замість порівняння рядків (що є повільною операцією), Elixir порівнює цілочисельні ідентифікатори. Переваги та особливості використання атомів Переваги атомів: Ефективність. Завдяки своєму числовому представленню, порівняння атомів є дуже швидким. Це осо...

Встановлення PostgreSQL на Ubuntu-сервер

Встановлення Оновлюємо пакети та встановлюємо PostgreSQL: sudo apt update sudo apt install -y postgresql postgresql-contrib Перевіряємо статус сервісу: sudo systemctl status postgresql Якщо PostgreSQL не запущений, запустимо його: sudo systemctl start postgresql sudo systemctl enable postgresql Налаштування безпеки Зміна пароля: sudo -u postgres psql У консолі PostgreSQL: ALTER USER postgres PASSWORD 'міцний_пароль'; \q \q - вихід з консолі. Список основних команд для роботи з PostgreSQL можна переглянути за посиланням. За замовчуванням PostgreSQL слухає localhost (127.0.0.1). Щоб дозволити доступ із зовнішніх машин, редагуємо конфігурацію: sudo nano /etc/postgresql/17/main/postgresql.conf (замість 17 вкажи версію PostgreSQL, яку встановлено) Шукаємо рядок: #listen_addresses = 'localhost' та замінюємо на listen_addresses = '*' Зберігаємо (Ctrl + X, Y, Enter). Тепер редагуємо pg_hba.conf: sudo nano /etc/postgresql/17/main/pg_hba.conf...

Прості типи даних в Elixir

Мова Elixir має низку простих (примітивних) типів даних, які часто використовуються в повсякденному програмуванні. Числа Elixir підтримує цілі (integer) та дійсні числа (float). # Цілі числа a = 42 b = -7 # Дійсні числа c = 3.14 d = -0.001 Булеві значення Elixir має два булевих значення: true та false . x = true y = false z = x and y # false Атоми Атоми — це константи з іменем, що починається з двокрапки. Вони широко використовуються, наприклад, для імен параметрів або станів. :ok :error :running :elixir_is_fun Рядки Рядки в Elixir — це двійкові дані з кодуванням UTF-8, оголошуються в подвійних лапках. name = "Pavlo" greeting = "Привіт, #{name}!" Nil Nil — це спеціальне значення, що позначає "відсутність значення". value = nil is_nil(value) # true Бінарні дані та байти Бінарні дані оголошуються в подвійних лапках або як бінарні літерали. string = "Привіт" # це рядок, але також бінарні дані binary = ...