Обработка ошибок в Node.js: не дать серверу упасть
Содержание
О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».
Основы обработки ошибок — try/catch, throw, промисы — одинаковы в браузере и Node. Но на сервере цена ошибки выше: одна необработанная способна уронить процесс и оставить без сервиса всех пользователей. Разберём серверную специфику.
Ошибки в асинхронном коде
Большинство серверного кода — асинхронное, и ошибки там ловят по-разному в зависимости от стиля:
// async/await — обычный try/catch (современный способ)
async function getUser(id) {
try {
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
return user;
} catch (err) {
logger.error('Ошибка БД:', err);
throw new Error('Не удалось получить пользователя'); // пробрасываем понятную
}
}
// колбэки старого стиля — ошибка первым аргументом
fs.readFile('file.txt', (err, data) => {
if (err) return handleError(err); // ВСЕГДА проверяем err
process(data);
});
В async/await асинхронная ошибка ловится обычным try/catch — как в браузере. В колбэках Node — соглашение «ошибка первым аргументом»: всегда проверяйте err до использования данных. Игнорировать err в колбэке — прямой путь к загадочным сбоям.
Два типа ошибок: операционные и программные
Ключевое для сервера различие:
Операционные — ожидаемы
Проблемы среды, обрабатываем
- БД недоступна, таймаут сети
- Файл не найден, нет прав
- Неверный ввод пользователя
- → обработать и продолжить работу
Программные — баги
Ошибки в коде, чиним
- Обращение к свойству undefined
- Вызов не-функции
- Опечатка, неверная логика
- → не маскировать, чинить; иногда перезапуск
- операционные ошибки — ожидаемые проблемы окружения: база недоступна, файла нет, внешний API не ответил, пользователь прислал мусор. Их обрабатывают и продолжают: повтор, ответ с понятной ошибкой, значение по умолчанию;
- программные ошибки (баги) — дефекты кода: обращение к
undefined, вызов не-функции. Их нельзя маскироватьtry/catch, их надо чинить. Пойманный и заглушённый баг лишь прячет проблему.
Смешивать эти типы — частая ошибка: либо глушат баги как операционные (и теряют сигнал о дефекте), либо роняют процесс от ожидаемой проблемы среды.
Чего НЕ делать: глушить uncaughtException
// ❌ ОПАСНО — заглушить и продолжить работу
process.on('uncaughtException', (err) => {
console.log('ой, ошибка');
// и просто работаем дальше — НЕЛЬЗЯ
});
После необработанного исключения процесс в неопределённом состоянии. Часть операции оборвана на середине, ресурсы (соединения, файлы) могли не освободиться, данные — оказаться в промежуточном виде. Продолжать работу в таком состоянии опасно: утечки, порча данных, непредсказуемое поведение. Правильная реакция — залогировать и корректно завершиться:
// ✅ логируем и завершаемся — менеджер процессов перезапустит
process.on('uncaughtException', (err) => {
logger.error('Критическая ошибка, завершаюсь:', err);
// закрыть соединения, потом выйти
process.exit(1); // менеджер (PM2, cluster) поднимет заново
});
process.on('unhandledRejection', (reason) => {
logger.error('Необработанное отклонение промиса:', reason);
process.exit(1);
});
uncaughtException и unhandledRejection — не механизм обработки, а последний рубеж: сюда попадает то, что вы упустили. Их роль — залогировать и дать чисто перезапуститься, а не «замять и работать дальше».
Стратегия «let it crash»
Отсюда следует контринтуитивный, но верный принцип: при неожиданной ошибке лучше упасть и перезапуститься, чем работать в сломанном состоянии.
// сервер + автоперезапуск через cluster/PM2
if (cluster.isPrimary) {
cluster.fork();
cluster.on('exit', () => cluster.fork()); // упал — подняли заново
}
Свежий процесс всегда в чистом, предсказуемом состоянии. Поэтому в проде Node запускают под менеджером (cluster или PM2), который мгновенно перезапускает упавший процесс. Один падает — его тут же заменяет чистый, сервис для пользователей не прерывается. Это надёжнее, чем героически пытаться продолжить после бага.
Где ставить try/catch
// ❌ не оборачивайте всё подряд — это шум
try { const x = 1 + 1; } catch (e) {}
// ✅ оборачивайте точки ОЖИДАЕМЫХ ошибок
try {
const data = await fetchExternalApi(); // сеть может подвести
} catch (e) {
return fallbackData(); // осмысленный запасной путь
}
try/catch — в точках, где ошибка ожидаема и её можно осмысленно обработать: запрос к БД, обращение к внешнему API, парсинг ввода. Оборачивать каждую строку — загромождать код без пользы. Остальное всплывает к центральному обработчику — в Express это middleware ошибок, которое ловит всё непойманное и отвечает клиенту корректно, а не роняет сервер.
Практический чек-лист
- проверяйте
errв колбэках, оборачивайтеawaitвtry/catchтам, где ошибка ожидаема; - различайте операционные ошибки (обработать) и баги (чинить);
- не глушите
uncaughtException/unhandledRejection— логируйте и завершайтесь; - запускайте прод под менеджером с автоперезапуском;
- централизуйте обработку (middleware ошибок), а не ловите всё локально;
- всегда бросайте объект
Error, не строку — ради стека вызовов.
Смежные темы: основы обработки ошибок — try/catch и throw; graceful shutdown — чистое завершение; middleware ошибок Express; логирование. Полный список — в уроках Node.js.