JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Создание и выбрасывание ошибок в Node.js

Создание и выбрасывание ошибок в Node.js

Модуль 4: Node.js, Next.js и Angular
2 уровень , 7 лекция
Открыта

1. Введение

В Node.js, как и в других языках программирования, обработка ошибок — это не "дополнение", а ключевая часть архитектуры любого приложения: от простого скрипта до распределённого сервера.

Ошибка — это сигнал: что-то пошло не по плану, и обычный поток выполнения должен быть прерван.
В Node.js мы бросаем (throw) ошибки для того, чтобы:

  • остановить выполнение функции при проблеме;
  • сообщить вызывающему коду: "Я не могу работать дальше";
  • передать управление в catch (или в глобальный обработчик, если его нет).

Без throw всё было бы гораздо сложнее: баги бы "терялись", а сервер мог бы возвращать некорректные данные или падать молча.

Как работает механизм throw в Node.js

Когда вы вызываете throw в коде Node.js (или браузерного JS), происходит следующее:

  1. Выполнение текущей функции немедленно прерывается.
  2. JavaScript ищет ближайший внешний блок try/catch. Если он есть, ошибка "поймана" и передаётся в catch.
  3. Если нет — Node.js завершает процесс с сообщением об ошибке и стеком вызовов.

Пример:


function readConfig(path) {
  if (!path) throw new Error('Путь к файлу не передан!');
  // остальной код
}

try {
  readConfig();
} catch (e) {
  console.error('Ошибка:', e.message);
}

Если закомментировать try/catch, приложение просто упадёт, что допустимо для CLI-скриптов, но очень плохо для серверных приложений!

2. Разбираемся с Error и его "родственниками"

В JavaScript, как и в других языках, есть встроенные типы ошибок. Все они, так или иначе, являются наследниками базового класса Error. Это как если бы "Ошибка" была большой семьей, а TypeError, RangeError и другие — её многочисленными детьми и внуками.

Таблица встроенных типов ошибок

Тип Ошибки Описание Пример использования (ситуация)
Error
Базовый тип для всех ошибок. Используется для общих, неопределенных ошибок.
throw new Error("Что-то пошло не так.");
TypeError
Когда значение не является ожидаемым типом.
throw new TypeError("Ожидался номер, получили строку.");
RangeError
Когда число выходит за допустимый диапазон.
throw new RangeError("Возраст должен быть от 0 до 150.");
ReferenceError
Попытка обращения к несуществующей переменной. (Часто сам JS бросает).
throw new ReferenceError("Переменная 'user' не определена.");
SyntaxError
Ошибка в синтаксисе кода. (Часто сам JS бросает на этапе парсинга).
eval("консоль.лог('привет');")
URIError
Ошибки при работе с функциями для кодирования/декодирования URI.
decodeURIComponent('%')
EvalError
Ошибки, связанные с функцией eval(). (Редко используется).
throw new EvalError("Ошибка в eval");

Когда вы используете throw new Error(), вы, по сути, бросаете самый общий тип ошибки. Это похоже на то, как если бы вы закричали: "Беда!" — не уточняя, что именно случилось. Для простых случаев это нормально, но для более сложных и больших приложений мы хотим быть более конкретными. Мы хотим кричать: "Беда! Пользователь ввёл неверный email!" или "Беда! Товар закончился!" – чтобы получатель ошибки сразу понял, в чём дело.

3. Создание собственных ошибок (extends Error)

Вот мы и подошли к самому интересному – как создать свои собственные, пользовательские типы ошибок. Это позволит вам делать код более читабельным, легко отлаживаемым и, главное, позволит вашим коллегам или вам самим в будущем точнее обрабатывать различные сценарии ошибок.

В JavaScript, как и во многих других объектно-ориентированных языках, мы можем создавать новые классы на основе существующих. Для ошибок это означает, что мы можем "наследовать" от Error или любого другого встроенного типа ошибки.

Пример 2: Наследуем от Error

Давайте вернемся к нашему приложению для управления пользователями. Допустим, мы хотим проверять не только возраст, но и другие данные пользователя, и для каждой конкретной проблемы выбрасывать свою, специализированную ошибку.

Представим, что у нас есть функция для создания пользователя, и она требует имя и возраст.


// Определяем нашу пользовательскую ошибку для проблем с пользователем
class UserValidationError extends Error {
  constructor(message, field = 'general') {
    super(message); // Вызываем конструктор родительского класса Error
    this.name = "UserValidationError"; // Даем нашему классу ошибок собственное имя
    this.field = field; // Добавляем дополнительное свойство: поле, в котором произошла ошибка
  }
}

// Теперь функция, которая создает пользователя, но с проверками
function createUser(username, age) {
  if (!username || username.length < 3) {
    // Бросаем нашу новую, специализированную ошибку!
    throw new UserValidationError("Имя пользователя должно быть не менее 3 символов", "username");
  }

  if (age < 0 || age > 150) {
    throw new UserValidationError("Возраст должен быть от 0 до 150", "age");
  }

  console.log(`Пользователь "${username}" (${age} лет) создан успешно!`);
  return { username, age }; // Возвращаем объект пользователя
}

// Попробуем использовать функцию с разными сценариями
try {
  createUser("Алиса", 25);   // Успех
  createUser("Боб", 160);    // Ошибка: возраст
  createUser("И", 30);      // Этот код не выполнится
} catch (error) {
  // Теперь мы можем проверить, что за тип ошибки мы поймали
  if (error instanceof UserValidationError) {
    console.error(`Ошибка валидации пользователя в поле "${error.field}": ${error.message}`);
  } else {
    // Если это какая-то другая, неожиданная ошибка
    console.error(`Неизвестная ошибка: ${error.message}`);
  }
}

console.log("\n--- Пробуем еще раз с другой ошибкой ---");

try {
  createUser("", 40);        // Ошибка: пустое имя
} catch (error) {
  if (error instanceof UserValidationError) {
    console.error(`Ошибка валидации пользователя в поле "${error.field}": ${error.message}`);
  } else {
    console.error(`Неизвестная ошибка: ${error.message}`);
  }
}

Разберем наш класс UserValidationError:

  • class UserValidationError extends Error: Мы объявляем новый класс UserValidationError, который наследует все свойства и методы от Error. Это значит, что он автоматически будет иметь message и stack.
  • constructor(message, field = 'general'): Наш конструктор принимает message (сообщение об ошибке) и дополнительный, специфичный для нашей ошибки field (поле, в котором произошла проблема). У field есть значение по умолчанию 'general', если его не передали.
  • super(message): Обязательный вызов! Это "команда" для родительского конструктора (в данном случае Error) инициализировать его часть объекта. Без super(), this будет недоступен, и вы получите ошибку. Это как если бы вы строили дом и забыли заложить фундамент – ничего не получится.
  • this.name = "UserValidationError";: Это очень важная строка! По умолчанию, при наследовании, свойство name у объектов ошибки всё равно остаётся 'Error'. Мы явно переопределяем его на имя нашего класса, чтобы при отладке или логировании сразу было видно, что это за тип ошибки. Например, error.name вернёт 'UserValidationError'.
  • this.field = field;: Мы добавляем наше собственное свойство field, которое делает эту ошибку более информативной.

Почему это круто?

  1. Конкретика: Теперь, когда мы ловим UserValidationError, мы точно знаем, что проблема связана с данными пользователя. Мы можем даже узнать, какое именно поле вызвало проблему (error.field). Это гораздо лучше, чем просто "Что-то пошло не так".
  2. Гибкость обработки: В блоке catch мы можем использовать instanceof для того, чтобы выполнить разную логику для разных типов ошибок. Если это UserValidationError, мы можем показать пользователю сообщение рядом с формой. Если это другая ошибка, может быть, отправить её в систему логирования.
  3. Читабельность: Читая код throw new UserValidationError(...), сразу понятно, о каком типе проблемы идёт речь.

4. Расширенные возможности: добавление данных к ошибкам

Иногда message и name недостаточно, чтобы полностью описать проблему. Как мы уже видели с field, вы можете добавлять любые дополнительные свойства к вашим пользовательским ошибкам. Это позволяет передавать еще больше контекста.

Пример 3: Ошибка с дополнительными данными и HTTP-статусом

В реальных веб-приложениях, особенно когда вы работаете с API (а вы с ними будете работать постоянно!), часто нужно возвращать определённый HTTP-статус код вместе с ошибкой. Например, 400 Bad Request для ошибок валидации, 404 Not Found для отсутствующих ресурсов. Давайте создадим ошибку, которая будет нести в себе такой статус.


// Определяем ошибку для API-запросов с кодом статуса
class ApiError extends Error {
  constructor(message, statusCode = 500, details = null) {
    super(message);
    this.name = "ApiError";
    this.statusCode = statusCode; // Добавляем HTTP-статус код
    this.details = details;       // Дополнительные детали, например, список полей с ошибками
  }
}

// Упрощенная функция для "получения" данных из "базы"
function getUserById(id) {
  if (typeof id !== 'number' || id <= 0) {
    throw new ApiError("ID пользователя должен быть положительным числом", 400, { errorCode: 'INVALID_ID_FORMAT' });
  }

  const users = {
    1: { id: 1, name: "Петр", email: "petr@example.com" },
    2: { id: 2, name: "Елена", email: "elena@example.com" },
  };

  const user = users[id];
  if (!user) {
    throw new ApiError(`Пользователь с ID ${id} не найден`, 404, { errorCode: 'USER_NOT_FOUND' });
  }

  return user;
}

// Использование
async function fetchAndDisplayUser(userId) {
  try {
    const user = getUserById(userId);
    console.log(`\nУспешно получен пользователь: ${JSON.stringify(user)}`);
  } catch (error) {
    if (error instanceof ApiError) {
      console.error(`\nAPI Ошибка! Статус: ${error.statusCode}, Сообщение: ${error.message}`);
      if (error.details) {
        console.error(`Детали ошибки: ${JSON.stringify(error.details)}`);
      }
    } else {
      console.error(`\nНеожиданная ошибка: ${error.message}`);
    }
  }
}

fetchAndDisplayUser(1);  // Успешное получение
fetchAndDisplayUser(3);  // Пользователь не найден (404)
fetchAndDisplayUser(-1); // Невалидный ID (400)

В этом примере ApiError несет в себе не только сообщение, но и statusCode и details, что очень удобно для отладки и для отправки правильных ответов клиенту в REST API. Обратите внимание, что getUserById не является асинхронной функцией, но мы обернули её вызов в async function fetchAndDisplayUser, чтобы показать, как можно работать с ошибками в контексте асинхронного кода, который вы уже знаете.

5. Важность error.stack и error.name

Мы уже упоминали error.stack и error.name, но давайте еще раз подчеркнем их важность.

error.name: Это строковое имя типа ошибки. Мы специально устанавливаем его в конструкторе нашего пользовательского класса (this.name = "ApiError";). Это свойство невероятно полезно, когда вы хотите быстро определить тип ошибки без использования instanceof (например, при логировании, когда у вас нет прямого доступа к классу ошибки, или при отправке ошибок по сети). Например, в ваших логах вы увидите ApiError: Пользователь не найден, а не просто Error: Пользователь не найден.

error.stack: Это строковое представление стека вызовов на момент возникновения ошибки. Он показывает, какие функции были вызваны, в каком порядке, и на какой строке кода в каждой из них находилось выполнение, когда произошла ошибка. Это как "карта пути", которая привела к происшествию.

  • Почему это полезно? Если ошибка возникает глубоко внутри вложенных вызовов функций, stack поможет вам быстро найти "корень зла". Он показывает не только место, где вы бросили ошибку (throw new ApiError(...)), но и то, откуда была вызвана эта функция, и откуда была вызвана та функция, и так далее, до самого начала выполнения скрипта.
  • Особенность: Когда вы создаёте свою ошибку, важно вызвать super(message) в конструкторе, потому что именно конструктор базового класса Error отвечает за корректное формирование stack.

6. Типичные ошибки при работе с пользовательскими ошибками

Несмотря на простоту, новички часто наступают на одни и те же грабли при работе с ошибками.

Ошибка №1: Забыли вызвать super(message) в конструкторе пользовательской ошибки.
Если вы создаете класс, который наследует от Error (или любого другого встроенного класса), и не вызываете super() в его конструкторе, вы получите ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. Это означает, что this не будет инициализирован, и вы не сможете добавлять к нему свои свойства или даже корректно настроить сообщение и стек вызовов. Всегда, всегда начинайте конструктор наследника с super().

Ошибка №2: Не присвоили this.name в пользовательской ошибке.
Если вы этого не сделаете, то при проверке error.name вы увидите "Error", а не имя вашего кастомного класса (например, "UserValidationError"). Это затруднит отладку и логирование, так как все ваши пользовательские ошибки будут выглядеть как обычные Error. Присвоение this.name = "ИмяВашегоКлассаОшибки" — это хороший стандарт.

Ошибка №3: Бросаете строки или числа вместо объектов Error.
Технически, JavaScript позволяет бросить что угодно (throw "Ой, беда!"; или throw 404;). Однако это очень плохая практика! При бросании примитивных значений вы теряете всю ценную информацию, которую несут объекты Error (сообщение, стек вызовов, ваши кастомные свойства). Отлаживать такие ошибки будет крайне сложно. Всегда бросайте объекты, наследующие от Error.

Ошибка №4: "Проглатывание" ошибок (Swallowing Errors).
Это когда вы ловите ошибку в catch блоке, но ничего с ней не делаете: ни логируете, ни обрабатываете, ни выбрасываете дальше.


try {
  doSomethingRisky();
} catch (error) {
  // Ничего не делаем. Ошибка просто исчезла!
}

Такой код делает вашу программу "немой" к проблемам. Ошибка возникает, но вы о ней никогда не узнаете, пока что-то не перестанет работать совсем. Всегда либо обрабатывайте ошибку (например, выводите пользователю сообщение, логируйте), либо, если вы не можете её обработать на текущем уровне, выбрасывайте её дальше (throw error;) или выбрасывайте новую, более специфичную ошибку, которая обертывает оригинальную (это называется "перебрасывание" или "error wrapping").

Ошибка №5: Использование ошибок для управления потоком логики, а не для исключительных ситуаций.
Исключения — для исключительных ситуаций, то есть для того, что не должно происходить в нормальном потоке выполнения программы. Например, отсутствие файла, деление на ноль, неверный тип данных. Не стоит использовать throw и catch для обычных условий, таких как "пользователь не найден" (если это ожидаемый сценарий, просто верните null или пустой массив) или "данные невалидны" (если это частая проверка, используйте if/else). Перехват исключений — это дорогая операция для интерпретатора, и злоупотребление ими может замедлить вашу программу.

1
Задача
Модуль 4: Node.js, Next.js и Angular, 2 уровень, 7 лекция
Недоступна
Создание пользовательского типа ошибки
Создание пользовательского типа ошибки
1
Задача
Модуль 4: Node.js, Next.js и Angular, 2 уровень, 7 лекция
Недоступна
Пользовательская ошибка с дополнительными данными и обработка разных типов ошибок
Пользовательская ошибка с дополнительными данными и обработка разных типов ошибок
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ