Back

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

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

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

Пользовательские ошибки JavaScript решают эту проблему. Они позволяют создавать структурированные, идентифицируемые типы ошибок, которые несут осмысленный контекст через всё приложение. Это руководство охватывает современный подход с использованием возможностей ES2022+, включая Error.cause для цепочки ошибок и классы ошибок JavaScript, которые надёжно работают в современных браузерах и средах выполнения.

Ключевые выводы

  • Расширяйте встроенный класс Error с помощью синтаксиса class ... extends Error для создания чистых, поддерживаемых пользовательских ошибок
  • Передавайте и message, и options в super() для сохранения трассировки стека и включения цепочки ошибок
  • Используйте Error.cause (ES2022+) для оборачивания и сохранения исходных ошибок из внешних источников
  • Добавляйте структурированные поля, такие как statusCode или field, вместо кодирования контекста в строках сообщений
  • Держите иерархии ошибок неглубокими — базовая ошибка с несколькими специализированными подтипами покрывает большинство потребностей

Стандартный паттерн: расширение Error

Современная обработка ошибок в JavaScript начинается с синтаксиса классов. Забудьте о манипуляциях с прототипами из прошлого — class ... extends Error это базовый подход:

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

Вызов super(message, options) критически важен. Он передаёт и сообщение, и объект опций родительскому конструктору Error, который автоматически обрабатывает захват трассировки стека.

Установка this.name гарантирует, что ваша ошибка правильно идентифицирует себя в трассировках стека и логировании. Без этого ваша пользовательская ошибка будет отображаться как обычная “Error”.

Добавление структурированных полей

Простые строки сообщений хрупки. Парсинг “Invalid email: user@” для извлечения контекста подвержен ошибкам. Вместо этого добавляйте структурированные поля:

class HttpError extends Error {
  constructor(message, statusCode, options) {
    super(message, options)
    this.name = 'HttpError'
    this.statusCode = statusCode
  }
}

throw new HttpError('Resource not found', 404)

Теперь ваш обработчик ошибок может проверять error.statusCode напрямую, а не парсить строки. Этот паттерн хорошо масштабируется — добавляйте details, code или любые специфичные для вашего домена поля, которые нужны приложению.

Цепочка ошибок с Error.cause

При оборачивании ошибок из внешних источников — баз данных, API, библиотек — необходимо сохранять исходную ошибку. Опция cause, введённая в ES2022, справляется с этим:

async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`)
    if (!response.ok) {
      throw new HttpError('User fetch failed', response.status)
    }
    return response.json()
  } catch (error) {
    throw new HttpError('Unable to load user', 500, { cause: error })
  }
}

Исходная ошибка остаётся доступной через error.cause, сохраняя полную цепочку ошибок для отладки. Это поддерживается во всех современных браузерах и Node.js — полифиллы не нужны.

Построение небольших иерархий ошибок

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

class AppError extends Error {
  constructor(message, options = {}) {
    super(message, { cause: options.cause })
    this.name = this.constructor.name
    this.statusCode = options.statusCode ?? 500
  }
}

class NotFoundError extends AppError {
  constructor(message, options = {}) {
    super(message, { ...options, statusCode: 404 })
  }
}

class ValidationError extends AppError {
  constructor(message, field, options = {}) {
    super(message, { ...options, statusCode: 400 })
    this.field = field
  }
}

Использование this.constructor.name автоматически устанавливает имя ошибки из класса, сокращая шаблонный код в подклассах.

Различение ошибок в асинхронных потоках

Пользовательские ошибки особенно полезны в асинхронном коде, где нужно обрабатывать разные режимы отказа:

try {
  const user = await fetchUser(userId)
} catch (error) {
  if (error instanceof NotFoundError) {
    return { status: 404, body: { message: 'User not found' } }
  }
  if (error instanceof ValidationError) {
    return { status: 400, body: { message: error.message, field: error.field } }
  }
  // Неожиданная ошибка — логируем и возвращаем общий ответ
  console.error('Unexpected error:', error.cause ?? error)
  return { status: 500, body: { message: 'Internal error' } }
}

Проверка instanceof корректно работает через всю цепочку наследования. Вы также можете различать ошибки, проверяя пользовательские поля или error.name при работе с сериализованными ошибками.

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

Рекомендации для TypeScript

Если вы используете TypeScript, установите target: "ES2022" или выше, или включите "ES2022" в массив lib в tsconfig.json. Это обеспечит правильную типизацию для свойства cause в интерфейсе ErrorOptions.

Заключение

Пользовательские ошибки JavaScript, построенные с помощью class ... extends Error, дают вам структурированные, идентифицируемые исключения, которые несут контекст через всё приложение. Передавайте message и options в super(), используйте cause для цепочки ошибок и добавляйте специфичные для домена поля вместо кодирования информации в строках сообщений. Держите иерархии неглубокими, и ваша обработка ошибок станет предсказуемой и отлаживаемой.

Часто задаваемые вопросы

Пользовательские ошибки позволяют программно различать разные типы сбоев. Вместо парсинга сообщений об ошибках для определения того, что пошло не так, вы можете использовать проверки instanceof или проверять пользовательские свойства, такие как statusCode или field. Это делает обработку ошибок более надёжной, а ваш код легче отлаживать и поддерживать.

Error.cause был введён в ES2022 и поддерживается во всех современных браузерах и Node.js 16.9+. Для более старых сред опция cause просто игнорируется — ваш код всё равно будет работать, но свойство cause будет undefined. Рассмотрите полифилл только если вам необходимо поддерживать устаревшие среды выполнения.

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

Да. Когда несколько операций могут завершиться неудачей независимо, оборачивайте отдельные сбои в пользовательские ошибки, затем объединяйте их с помощью AggregateError. Каждая ошибка в массиве errors сохраняет свой тип, поэтому вы всё ещё можете использовать проверки instanceof при обработке результатов.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay