ПрограммированиеBackend разработчик

Опишите, как работает механизм типов пользовательских ошибок (Custom Errors) в TypeScript. Как правильно объявлять пользовательские классы ошибок, зачем указывать их тип, и как избежать ловушек с 'instanceof' и сериализацией ошибок?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

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

Проблема: При наследовании от Error могут возникнуть трудности: теряется прототип, неверно работает 'instanceof', типы могут быть описаны некорректно, сериализация часто приводит к потере stack trace. Без явного указания свойств можно получить баги при обработке ошибок.

Решение: Корректно определять пользовательские ошибки через расширение класса Error. Следует явно прописывать имя, восстанавливать прототип вручную (важно для ES5 и при компиляции в CommonJS), и типизировать поля ошибок.

Пример кода:

class ValidationError extends Error { code: number; constructor(message: string, code: number = 400) { super(message); Object.setPrototypeOf(this, ValidationError.prototype); this.name = 'ValidationError'; this.code = code; } } function process(user: string) { if (!user) throw new ValidationError('User required', 401); }

Ключевые особенности:

  • Явное восстановление прототипа для поддержки 'instanceof' даже при транспиляции.
  • Типизация пользовательских свойств ошибок.
  • Индивидуальный класс для каждой логической группы ошибок.

Вопросы с подвохом.

Почему нельзя опустить вызов Object.setPrototypeOf(this, ...)?

Если не вызывать Object.setPrototypeOf(this, Class.prototype), 'instanceof' ValidationError не сработает при компиляции в ES5/CommonJS или Babel. Это приведёт к тому, что блоки catch ValidationError не перехватят ошибку.

class CustomErr extends Error {} const err = new CustomErr('msg'); console.log(err instanceof CustomErr); // false без setPrototypeOf

Можно ли опускать поле name у пользовательской ошибки?

Если не установить свойство this.name, стек ошибки и отображение в логах будут некорректны, что осложнит поиск причины и классификацию ошибок.

Нужно ли делать ошибки сериализуемыми, и если да — как?

Ошибки должны корректно сериализоваться (например, для логгирования или передачи по сети), иначе JSON.stringify(new Error()) не выдаст message и stack. Следует переопределять toJSON метод.

class SerializableError extends Error { toJSON() { return { name: this.name, message: this.message, stack: this.stack }; } }

Типовые ошибки и анти-паттерны

  • Отсутствие Object.setPrototypeOf приводит к неверной работе instanceof
  • Игнорирование name и пользовательских свойств
  • Сериализация ошибки без реализации toJSON: теряются stack/message

Пример из жизни

Негативный кейс

В проекте делали просто class MyError extends Error, не восстанавливая прототип. Ошибки ловились через if (err instanceof MyError), но это не срабатывало, и код silently пропускал обработку критических ситуаций.

Плюсы:

  • Код выглядел лаконично
  • Не было boilerplate

Минусы:

  • instanceof не работал
  • Исключения не логировались корректно
  • При смене ts->js транспилятора появилась несовместимость

Позитивный кейс

Реализовали корректный CustomError, явно задали name, code и toJSON, тесты покрыли обработку разных типов ошибок. В логах и обработчиках catch структура ошибок стала понятной, за счёт этого снизилось время поиска багов.

Плюсы:

  • Чёткая типовая иерархия ошибок
  • Надёжная сериализация
  • Кроссплатформенность между браузером и Node.js

Минусы:

  • Появилась шаблонная логика в каждом CustomError-классе