Асинхронная природа Node.js, основанная на цикле событий, требует особого подхода к обработке ошибок. Даже опытные разработчики часто оставляют пробелы в обработке исключений, что приводит к падению приложений в продакшене. Решение проблемы требует понимания не только синтаксиса, но и внутренних механизмов выполнения кода.
Где теряются ошибки: базовые сценарии
Рассмотрим классический пример асинхронного чтения файла:
const fs = require('fs');
function readConfig() {
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) console.error('Ошибка чтения');
return JSON.parse(data);
});
}
Оставленное без обработки исключение JSON.parse
при невалидном JSON приводит к необработанному исключению. В Node.js 16+ это вызывает завершение процесса.
Исправление требует вложенной обработки:
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) return console.error('Read error:', err);
try {
const config = JSON.parse(data);
} catch (parseErr) {
console.error('Parse error:', parseErr);
}
});
Проблемы с промисами и async/await
Современная практика предполагает использование async/await
, но распространена ошибочная структура:
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
// Вызов без try/catch
fetchData().then(data => ...);
Необработанные ошибки вызовут unhandled rejection. Решение — обязательная обертка:
async function safeFetch() {
try {
const response = await fetch('/api/data');
return await response.json();
} catch (err) {
handleError(err);
throw err; // Сохраняем стек вызовов
}
}
Для обработки промисов используйте .catch()
с явной передачей ошибки:
fetchData()
.then(data => process(data))
.catch(err => {
logger.log(err);
throw new ApplicationError('Data processing failed', { cause: err });
});
Паттерны для enterprise-приложений
Централизованная обработка
В Express-приложениях создайте middleware для ошибок:
app.use(async (err, req, res, next) => {
if (err instanceof DatabaseError) {
await sendAlertToSRE(err);
return res.status(503).json({ error: 'Database unavailable' });
}
next(err);
});
Обертка асинхронных функций
Декоратор для обработки исключений в маршрутах:
function asyncHandler(fn) {
return (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
}
app.get('/data', asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
Транзакционные операции
Для задач, требующих атомарности:
async function transactionalUpdate(userId, update) {
const session = await mongoose.startSession();
try {
session.startTransaction();
const user = await User.findById(userId).session(session);
await user.updateOne(update).session(session);
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw new DatabaseTransactionError('Update failed', { cause: err });
} finally {
session.endSession();
}
}
Глубокие проблемы EventEmitter
Ошибки в обработчиках событий часто игнорируются:
const eventEmitter = new EventEmitter();
emitter.on('data', async (payload) => {
await processPayload(payload); // Необработанный rejection
});
Решение — обертка в микротаск:
emitter.on('data', (payload) => {
process.nextTick(async () => {
try {
await processPayload(payload);
} catch (err) {
handleError(err);
}
});
});
Инструменты мониторинга
Настройте обработчики глобальных событий:
process.on('unhandledRejection', (reason, promise) => {
sentry.captureException(reason, { extra: { promise } });
});
process.on('uncaughtException', (err) => {
sentry.captureException(err);
process.exit(1); // Обязательное завершение после фатальной ошибки
});
Заключение
Ключевые принципы устойчивой обработки ошибок в Node.js:
- Гранулярность: Обрабатывайте исключения на максимально низком уровне, где доступен контекст
- Декларативность: Используйте обертки и middlewares для устранения дублирования
- Композиция: Сохраняйте оригинальные ошибки через
cause
(Node.js 16+) - Мониторинг: Интегрируйте глобальные обработчики с системами трейсинга
- Консистентность: Документируйте политики обработки ошибок для команды
Планирование обработки исключений на этапе проектирования архитектуры экономит сотни часов отладки. Асинхронные ошибки не могут быть второстепенной задачей — их некорректная обработка превращает приложение в нестабильную систему с непредсказуемым поведением.