Создание пользовательских ошибок в JavaScript

Создание пользовательских ошибок в JavaScript

Технический фундамент throw и иерархии ошибок

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

  • системные ошибки (TypeError, ReferenceError) обычно сигнализируют о баге;
  • доменные ошибки (ValidationError, AuthError) описывают ожидаемые проблемные сценарии;
  • обработка через instanceof позволяет разделить реакции по типам.

Фундаментальный принцип: ошибка должна быть типизированным сигналом, а не случайной строкой.

Зачем создавать свои ошибки

Стандартные ошибки вроде TypeError полезны, но часто они слишком общие для бизнес-задач. Когда ты пишешь продуктовый код, важно различать типы сбоев: неверный email, недоступный тариф, просроченный токен, пустая корзина. Пользовательские ошибки позволяют явно описать, что именно пошло не так.

Ключевой момент: пользовательская ошибка это способ сделать поведение системы предсказуемым для тебя, команды и логирования.

Проверь себя: почему сообщение "Validation failed" хуже, чем "Email обязателен" в практической отладке?

throw как явный сигнал о проблеме

throw прерывает нормальное выполнение и выбрасывает исключение. Ты можешь бросать как встроенные ошибки, так и свои.

function requireEmail(email) {
  if (!email) {
    throw new Error('Email обязателен');
  }

  return email.trim().toLowerCase();
}

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

Проверь себя: когда лучше вернуть null, а когда бросить ошибку?

Минимальная практика: разные сообщения для разных причин

function validatePassword(password) {
  if (typeof password !== 'string') {
    throw new TypeError('Пароль должен быть строкой');
  }

  if (password.length < 8) {
    throw new RangeError('Пароль слишком короткий');
  }

  return true;
}

Здесь мы используем разные типы ошибок:

  • TypeError для неверного типа;
  • RangeError для недопустимой длины.

Такой подход помогает и в коде, и в обработке ошибок: можно по типу выбрать нужное сообщение для UI.

Кастомный класс ошибки

Когда ошибок много, удобно создавать свой класс через class ... extends Error.

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

function validateUser(user) {
  if (!user.email) {
    throw new ValidationError('Email обязателен', 'email');
  }
}

Новый термин: кастомный класс ошибки - свой тип исключения с дополнительными полями, например field, code, details.

Проверь себя: зачем явно задавать this.name, если класс уже называется ValidationError?

Подсказка: потому что super(message) создает Error и по умолчанию name часто остается равным 'Error'. Плюс в сборках/минификации имя класса может меняться, а name хочется стабильным для логов и обработки.

class ValidationError2 extends Error {}
console.log(new ValidationError2('oops').name); // Error

Обработка пользовательских ошибок через instanceof

Обычно в catch ты отличаешь "ожидаемые" бизнес-ошибки от системных сбоев.

try {
  validateUser({ email: '' });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Поле ${error.field}: ${error.message}`);
  } else {
    console.log('Неожиданная ошибка, нужен отдельный разбор');
  }
}

Дополнительный пример: передача машинно-читаемого code в кастомной ошибке.

class ApiError extends Error {
  constructor(message, code) {
    super(message);
    this.name = 'ApiError';
    this.code = code;
  }
}

Здесь часто путаются: не все ошибки нужно показывать пользователю как есть. Внутренние ошибки лучше логировать, а пользователю отдавать безопасное, понятное сообщение.

Реальный микро-сценарий

Сценарий: регистрация пользователя.

  • validateInput бросает ValidationError, если email/пароль невалидны;
  • UI показывает конкретные подсказки по полям;
  • неожиданные ошибки (например, баг в коде) идут в лог и показывают общий текст "Попробуй позже".

Это разделение резко повышает качество UX и скорость дебага.

Частые ошибки новичков

  • Бросать строку (throw 'error') вместо объекта Error.
  • Делать одинаковый тип ошибки для разных причин.
  • Перехватывать все и терять первичную ошибку.
  • Использовать throw там, где можно безопасно вернуть результат проверки.

Анти-провал: всегда задавай ошибке четкое сообщение и, при необходимости, машинно-удобный код причины (code).

Что будет, если изменить входные данные

  • Если в validatePassword передать число, сработает TypeError.
  • Если передать строку длиной 5, будет RangeError.
  • Если строка длиной 10, функция пройдет без ошибок.

Это и есть предсказуемое поведение: одна входная ситуация -> один тип реакции.

Проверь себя: почему это важно для автотестов и стабильной API-логики?

Краткий итог

  • Пользовательские ошибки помогают выражать бизнес-смысл, а не только технический сбой.
  • throw применяй осознанно: когда продолжать выполнение опасно.
  • Для сложных сценариев создавай свои классы ошибок через extends Error.
  • Разделяй обработку ожидаемых (ValidationError) и неожиданных ошибок.
  • Четкие типы и сообщения ошибок упрощают UI, логи и отладку всей системы.