Обработка ошибок в JavaScript: try/catch и продвинутые техники в Node.js

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в JavaScript: try/catch и не только: продвинутый уровень

Обработка ошибок — это не просто защита от падения приложения, а фундаментальный аспект построения надежных и отказоустойчивых систем на JavaScript и Node.js. Базовое использование try/catch знакомо каждому разработчику, но в реальных проектах, особенно на серверной стороне, возникает множество нюансов: асинхронные потоки, цепочки промисов, глобальные сбои и необходимость в структурированных кастомных ошибках. В этой статье мы разберем продвинутые техники обработки ошибок, которые помогут вам писать более чистый и предсказуемый код на Node.js.

1. Глубокое погружение в try/catch и асинхронность

Классический try/catch синхронен. Он прекрасно ловит ошибки внутри обычных функций, но пасует перед асинхронными колбэками или промисами без await. В Node.js, где 90% кода асинхронно, это часто становится причиной "проглоченных" исключений.

Ошибка: try/catch не ловит ошибку в setTimeout

function dangerousAsync() {
  setTimeout(() => {
    throw new Error('Сбой в таймере!');
  }, 1000);
}

try {
  dangerousAsync();
} catch (e) {
  console.log('Поймано:', e.message); // Этот код НЕ выполнится
}

В этом примере ошибка выбрасывается вне контекста try, и процесс Node.js аварийно завершится. Правильный подход — использовать промисы или async/await.

Решение: оборачиваем асинхронный код в Promise

function safeAsync() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('Сбой в таймере!'));
    }, 1000);
  });
}

(async () => {
  try {
    await safeAsync();
  } catch (e) {
    console.log('Поймано:', e.message); // Выполнится через 1 секунду
  }
})();

Главное правило: любую асинхронную операцию оборачивайте в промис, и используйте await внутри try/catch. Это гарантирует, что ошибка будет перехвачена в нужном контексте.

2. Кастомные классы ошибок: семантика и контекст

Вместо того чтобы выбрасывать new Error('Что-то пошло не так'), создавайте иерархию собственных классов ошибок. Это позволяет на уровне обработчика понимать тип сбоя и реагировать соответствующим образом (например, логировать, отправлять в мониторинг или возвращать пользователю определенный HTTP-статус).

class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

class DatabaseError extends AppError {
  constructor(message, originalError) {
    super(message, 503);
    this.originalError = originalError; // Сохраняем исходную ошибку для отладки
  }
}

// Использование
function findUser(id) {
  if (!id) throw new ValidationError('ID пользователя обязателен');
  // ... логика, которая может выбросить DatabaseError
}

Преимущества такого подхода:

  • Читаемость: имя класса сразу говорит о природе проблемы.
  • Маршрутизация: в глобальном обработчике вы можете проверять instanceof и выбирать стратегию ответа.
  • Дополнительные поля: вы можете добавлять любые данные (statusCode, originalError, userId и т.д.).

3. Глобальные обработчики ошибок в Node.js

Даже при самом тщательном контроле, необработанные ошибки могут просочиться наружу. В Node.js есть два ключевых события для их перехвата: uncaughtException и unhandledRejection.

uncaughtException — ловит синхронные ошибки, которые не были пойманы ни в одном try/catch. Важно: после вызова этого обработчика приложение находится в нестабильном состоянии. Рекомендуется залогировать ошибку и завершить процесс, а затем перезапустить его с помощью менеджера процессов (PM2, Docker).

unhandledRejection — ловит отклоненные промисы, у которых нет обработчика .catch(). В современных версиях Node.js это приводит к завершению процесса, поэтому обработчик обязателен.

process.on('uncaughtException', (err) => {
  console.error('Необработанная ошибка:', err);
  // Логируем в файл или внешнюю систему (Sentry, Datadog)
  // Завершаем процесс, так как состояние приложения неопределенно
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Необработанный rejection промиса:', reason);
  // Логируем и завершаем процесс
  process.exit(1);
});

Также, если вы используете Express.js, обязательно добавьте глобальный middleware для ошибок с четырьмя параметрами (err, req, res, next):

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Внутренняя ошибка сервера';

  // Логируем полный стек для разработчика
  console.error(err.stack);

  res.status(statusCode).json({
    status: 'error',
    statusCode,
    message: process.env.NODE_ENV === 'production' ? 'Что-то пошло не так' : message,
  });
});

4. Паттерны для работы с промисами и цепочками .catch

При работе с цепочками промисов (.then().catch()) часто возникает соблазн поставить один .catch() в конце. Это работает, но скрывает контекст ошибки. Более продвинутый подход — локализовать обработку ошибок и использовать фабрики обработчиков.

Пример: функция-обертка для асинхронных middleware в Express

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);                

Блоги

Рекомендаций по книгам