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

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

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

Начать курс

Введение: Почему обработка ошибок — основа надёжного кода

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

Основы: блоки try, catch и finally

Базовый синтаксис обработки ошибок в JavaScript выглядит так:

try {
  // Код, который может выбросить исключение
  const data = JSON.parse('{"name": "Node.js"}');
  console.log(data.name);
} catch (error) {
  // Обработка ошибки
  console.error('Произошла ошибка:', error.message);
} finally {
  // Код, который выполнится в любом случае
  console.log('Блок finally выполнен');
}

Как это работает:

  • try — содержит потенциально опасный код.
  • catch — перехватывает исключение, если оно возникло. В параметр error попадает объект ошибки.
  • finally — выполняется всегда, независимо от того, была ошибка или нет. Идеально для закрытия файлов, освобождения ресурсов или завершения транзакций.

Важно помнить: finally сработает даже после return в блоке try или catch.

Типы ошибок в JavaScript

Встроенные конструкторы ошибок помогают точнее классифицировать исключения:

  • Error — базовый класс для всех ошибок.
  • SyntaxError — синтаксическая ошибка (например, некорректный JSON).
  • TypeError — неверный тип данных (например, вызов не функции).
  • ReferenceError — обращение к несуществующей переменной.
  • RangeError — выход за допустимые пределы (например, рекурсия).

Пример обработки разных типов:

try {
  const obj = null;
  console.log(obj.property); // TypeError
} catch (err) {
  if (err instanceof TypeError) {
    console.log('Ошибка типа:', err.message);
  } else if (err instanceof ReferenceError) {
    console.log('Переменная не найдена');
  } else {
    console.log('Неизвестная ошибка:', err);
  }
}

Генерация собственных ошибок: оператор throw

Иногда необходимо явно указать, что в программе возникла исключительная ситуация. Для этого используется оператор throw. Он может выбросить что угодно: строку, число, объект, но лучше всего — экземпляр класса Error или его наследника.

function divide(a, b) {
  if (b === 0) {
    throw new Error('Деление на ноль запрещено');
  }
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Аргументы должны быть числами');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log(result);
} catch (err) {
  console.error(err.name + ': ' + err.message);
}

Почему лучше выбрасывать Error, а не строку? Объект Error содержит стек вызовов (stack), что критически важно для отладки в Node.js. Строка такой информации не даст.

Создание кастомных классов ошибок

В реальных проектах (например, веб-сервер на Express) удобно иметь собственные типы ошибок для разных ситуаций: ValidationError, NotFoundError, DatabaseError. Это упрощает централизованную обработку.

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
    this.statusCode = 400;
  }
}

class NotFoundError extends Error {
  constructor(resource) {
    super(`Ресурс "${resource}" не найден`);
    this.name = 'NotFoundError';
    this.statusCode = 404;
  }
}

// Использование
function getUser(id) {
  if (!id) throw new ValidationError('ID пользователя обязателен');
  // ... логика поиска
  throw new NotFoundError('user');
}

try {
  getUser(null);
} catch (err) {
  if (err instanceof ValidationError) {
    console.log('Ошибка валидации:', err.message);
  } else if (err instanceof NotFoundError) {
    console.log('Не найдено:', err.message);
  }
  console.log('HTTP статус:', err.statusCode);
}

Асинхронная обработка ошибок: промисы и async/await

Node.js построен на асинхронности. Для промисов существует два основных способа обработки ошибок:

1. Метод .catch() для промисов

const fs = require('fs').promises;

fs.readFile('/path/to/file.txt', 'utf8')
  .then(data => {
    console.log('Файл прочитан:', data);
  })
  .catch(err => {
    console.error('Ошибка чтения файла:', err.message);
  });

2. try/catch с async/await

Современный и самый читаемый подход:

const fs = require('fs').promises;

async function readConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf8');
    const config = JSON.parse(data);
    return config;
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.error('Файл конфигурации не найден');
    } else if (err instanceof SyntaxError) {
      console.error('Некорректный JSON в конфиге');
    } else {
      console.error('Неизвестная ошибка:', err);
    }
    throw err; // Пробрасываем дальше, если не можем обработать
  }
}

readConfig().catch(err => {
  console.log('Фатальная ошибка при запуске:', err.message);
  process.exit(1);
});

Обратите внимание: в catch можно пробросить ошибку дальше (throw err), если текущий уровень не может её корректно обработать. Это формирует цепочку ответственности.

Лучшие практики и частые ошибки

Вот несколько

Блоги

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