Будучи разработчиком Node.js, вы неизбежно столкнётесь с ошибками. Но как их обрабатывать ‒ не просто ловить, а делать это системно, обеспечивая отказоустойчивость приложения и содержательные логи? Этот вопрос не так прост, как кажется на первый взгляд, особенно с учётом эволюции подходов от колбэков через промисы к современному async/await.
Природа ошибок в асинхронном мире
В Node.js ошибки делятся на три фундаментальные категории:
- Операционные ошибки (сбой во время выполнения: сетевые проблемы, ошибки ввода-вывода)
- Ошибки программиста (баги в коде: неопределённые переменные, ошибки логики)
- Преднамеренные ошибки (контролируемые исключения для бизнес-логики)
Синхронные ошибки можно перехватывать через try/catch
:
function parseJSONSync(input) {
try {
return JSON.parse(input);
} catch (err) {
// Чистый пример ловли синхронной ошибки
logger.error('Parse failed', err);
return null;
}
}
Но асинхронный код нарушает эту простоту. Рассмотрим эволюцию подходов.
Колбэки: паттерн "error-first"
Традиционный подход Node.js ‒ колбэки с первым аргументом-ошибкой:
const fs = require('fs');
fs.readFile('/nonexistent.txt', (err, data) => {
if (err) {
// Обязательно проверка err
console.error('Ошибка чтения:', err.message);
return;
}
console.log(data.toString());
});
Критические нюансы:
-
Никогда не игнорируйте ошибку! Проблема
if (err) throw err;
в асинхронном колбэке ‒ исключение приведёт к краху процесса. Альтернатива ‒ безопасный проброс:javascriptfunction readFileAsync(path, callback) { fs.readFile(path, (err, data) => { if (err) return callback(err); // Пробрасываем // Обработка данных... callback(null, processedData); }); }
-
Потеря стека вызовов: Время выполнения колбэка стирает исходный стек. Решение ‒ создание пользовательских ошибок:
javascriptfs.readFile('config.json', (err, data) => { if (err) { const customErr = new Error('Config load failed'); customErr.originalError = err; customErr.file = 'config.json'; return callback(customErr); } // ... });
Промисы: мгновенная гибель и цепной проброс ошибок
Промисы предлагают более структурированный подход с .catch()
:
const fetch = require('node-fetch');
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) throw new Error('HTTP error');
return response.json();
})
.then(data => processData(data))
.catch(err => {
// Перехват ВСЕХ ошибок в цепочке
console.error('Fetch pipeline failed:', err);
});
Особенности:
-
Ошибки автоматически пробрасываются в ближайший
catch
-
throw
внутри обработчика.then()
преобразуется в отклонённый промис -
Коварная проблема: забытый
.catch()
:javascript// Опасный код! asyncFunction() .then(data => console.log(data)); // Непойманное обещание упадёт позднее
Всегда завершайте цепочку промисов обработчиком ошибок.
Async/await: синхронный стиль с асинхронными подводными камнями
Современный подход вводит async/await
:
async function loadUserData(userId) {
try {
const user = await fetchUser(userId);
const profile = await fetchProfile(user.profileId);
return { user, profile };
} catch (err) {
// Перехватывает ЛЮБУЮ ошибку в блоке try
console.error('User data load failed for', userId, err);
throw new UserDataError(userId, err);
}
}
Преимущества:
- Ошибки обрабатываются как в синхронном коде через
try/catch
- Сохранение стека вызовов
- Возможность аннотирования ошибок контекстом
Опасные заблуждения:
-
Иллюзия отлова всех ошибок:
javascriptasync function dangerousExample() { const promise = fetchData(); // Длинная операция... await new Promise(resolve => setTimeout(resolve, 1000)); const data = await promise; // Ошибка здесь НЕ поймается } // Правильно: обернуть все асинхронные действия сразу async function safeExample() { const data = await fetchData(); // ... }
-
Пропущенный
await
при отлове:javascriptasync function updateUser(user) { try { return saveUser(user); // Пропущенный await! } catch (err) { // Этот блок НЕ выполнится при ошибке saveUser } }
Продвинутые методики обработки
Агрегирование ошибок
При параллельном выполнении Promise.all()
прерывается при первой ошибке. Для получения всех ошибок используйте Promise.allSettled()
:
async function batchProcessing(items) {
const results = await Promise.allSettled(
items.map(item => processItem(item))
);
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
// Логируем все ошибки единообразно
throw new AggregateError(errors, 'Batch processing failed');
}
return results.map(r => r.value);
}
Трансформация ошибок
Создавайте иерархию ошибок для улучшения диагностики:
class AppError extends Error {
constructor(message, originalError) {
super(message);
this.originalError = originalError;
this.timestamp = new Date();
}
}
class DatabaseError extends AppError {
constructor(originalError, query) {
super('Database operation failed', originalError);
this.query = query;
this.errorCode = originalError.code;
}
}
// Использование
try {
await db.query('SELECT * FROM missing_table');
} catch (err) {
throw new DatabaseError(err, 'SELECT');
}
Централизованный перехват
В приложениях Express реализуйте middleware для консолидации обработки:
app.use(async (err, req, res, next) => {
// Логирование со структурированным контекстом
logger.error({
error: err.message,
stack: err.stack,
httpMethod: req.method,
path: req.path,
user: req.user?.id
});
// Преобразование в клиентский формат
const statusCode = err instanceof ClientError ? 400 : 500;
res.status(statusCode).json({
error: err.publicMessage || 'Internal server error'
});
});
Золотые правила практической обработки ошибок
- Всегда оборачивайте async операции в try/catch: Избегайте соблазна пропускать обработку "на потом".
- Обеспечьте контекст: Аннотируйте ошибку подробностями (параметры запроса, идентификаторы). Без контекста ошибка ‒ просто шум.
- Разделяйте ответственность: Не смешивайте обработку ошибок с бизнес-логикой. Выделите централизованный механизм для логирования и проброса.
- Используйте сообщения для заказчика: Пользовательские ошибки должны содержать понятное описание проблемы. Технические детали ‒ только для логов.
- Необработанные промахи реагируйте мгновенно: Обязательно вешайте обработчик
unhandledRejection
:javascriptprocess.on('unhandledRejection', (reason, promise) => { console.error('НЕОБРАБОТАННОЕ ОТКЛОНЕНИЕ ПО ОБЕЩАНИЮ:', reason); // Экстренное завершение или перезагрузка process.exit(1); });
Эффективная обработка ошибок ‒ не модуль из npm, который можно подключить. Это архитектурный подход, требующий постоянного внимания. Начните с дисциплинированного использования try/catch
в async-функциях, добавляйте контекст к ошибкам, используйте структурированное логирование ‒ и ваши сервисы превратятся из хрустальных башен в устойчивые форты, способные выдерживать реалии продакшена.