Обработка ошибок в 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.

Частые вопросы

Чем обработка ошибок в Node отличается от браузера?
Основы те же — try/catch и промисы, но на сервере ставки выше: необработанная ошибка может уронить процесс и оставить всех пользователей без сервиса. Поэтому важны различие типов ошибок и стратегия перезапуска, а не только ловля.
Почему нельзя глушить uncaughtException?
После необработанного исключения процесс в неопределённом состоянии — часть операций оборвана, ресурсы могли не освободиться. Продолжать работу опасно: возможны утечки и порча данных. Правильно — залогировать, корректно завершиться и дать менеджеру процессов перезапустить.
Что такое операционная ошибка и программная?
Операционная — ожидаемая проблема среды: недоступна база, файл не найден, неверный ввод. Её обрабатывают и продолжают. Программная — баг в коде вроде обращения к свойству undefined. Её не маскируют, а чинят, иногда перезапуская процесс.
Как ловить ошибки в асинхронном коде Node?
В async/await — обычным try/catch вокруг await. В колбэках старого стиля — проверять первый аргумент err. Необработанные отклонения промисов ловит событие unhandledRejection, но полагаться на него как на основную обработку нельзя.
Нужен ли try/catch вокруг каждой операции?
Нет, это загромождает код. Оборачивают точки, где ошибка ожидаема и её можно осмысленно обработать — запрос к базе, парсинг ввода. Остальное всплывает к центральному обработчику, например middleware ошибок в Express.