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
Коментарі
Дописати коментар