Skip to main content

Як створити власний тип помилки

Власний тип помилки, це звичайний клас, що успадковує Error: у конструкторі ви викликаєте super(message, { cause }), виставляєте this.name і додаєте власні поля на кшталт code, status або details. Такий клас поводиться як справжня помилка, зберігає стек і cause, і його можна точно розпізнати через instanceof.

Теорія

TL;DR

  • class MyError extends Error плюс super(message), це мінімум, який уже працює.
  • this.name = this.constructor.name робить назву помилки осмисленою в логах.
  • Другий аргумент super(message, { cause }) зберігає початкову помилку у ланцюжку.
  • Error.captureStackTrace(this, this.constructor) (V8) прибирає конструктор зі стека.
  • Власні поля (code, status, details) роблять помилку придатною для API і моніторингу.
  • Ієрархія AppError -> ValidationError / AuthError дає точну обробку в catch.

Швидкий приклад

javascript
class ValidationError extends Error { constructor(message, details) { super(message); this.name = "ValidationError"; this.details = details; } } try { throw new ValidationError("Email is required", { field: "email" }); } catch (e) { console.log(e instanceof ValidationError, e.name, e.details); // true ValidationError { field: "email" } }

Базовий шаблон (ES2022+)

javascript
class AppError extends Error { constructor(message, { code, cause } = {}) { super(message, { cause }); // зберігає stack і cause this.name = this.constructor.name; // "AppError" this.code = code ?? "APP_ERROR"; if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } } // Часткові випадки class ValidationError extends AppError { constructor(message, details, opts = {}) { super(message, { ...opts, code: "VALIDATION_ERROR" }); this.details = details; // будь-які ваші поля } } class AuthError extends AppError { constructor(message = "Unauthorized", opts = {}) { super(message, { ...opts, code: "AUTH_ERROR" }); this.status = 401; } }

Використання:

javascript
function parseUser(input) { if (!input.email) { throw new ValidationError("Email is required", { field: "email" }); } return input; } try { parseUser({}); } catch (e) { if (e instanceof ValidationError) { console.log(e.code, e.details); // VALIDATION_ERROR { field: "email" } } else { console.error("Unexpected error", e); } }

Обгортання вихідної помилки через cause

Це корисно, коли ви перехоплюєте низькорівневий виняток і хочете додати контекст, не втративши оригінал:

javascript
try { JSON.parse("not valid JSON"); } catch (e) { throw new AppError("Failed to parse the config", { cause: e, code: "CONFIG_PARSE" }); }

Пізніше можна дивитися весь ланцюжок:

javascript
console.error(err.cause); // вихідна помилка

Версія для TypeScript

typescript
type AppErrorCode = "APP_ERROR" | "VALIDATION_ERROR" | "AUTH_ERROR"; class AppError extends Error { code: AppErrorCode; declare cause?: unknown; // щоб TS знав про cause constructor(message: string, opts: { code?: AppErrorCode; cause?: unknown } = {}) { super(message, { cause: opts.cause }); this.name = new.target.name; this.code = opts.code ?? "APP_ERROR"; if ((Error as any).captureStackTrace) { (Error as any).captureStackTrace(this, new.target); } } } class ValidationError extends AppError { details?: Record<string, unknown>; constructor(message: string, details?: Record<string, unknown>, opts: { cause?: unknown } = {}) { super(message, { ...opts, code: "VALIDATION_ERROR" }); this.details = details; } }

Тут new.target.name дає ім'я саме того класу, який створюють, тому підкласи отримують правильний name без зайвого коду.

Практичні прийоми

  • Серіалізація (логування або відповідь API):

    javascript
    function errorToJson(err) { return { name: err.name, message: err.message, code: err.code, stack: err.stack, details: err.details, cause: err.cause instanceof Error ? err.cause.message : err.cause, }; }
  • Прив'язка до HTTP. Додайте status (400, 401, 404, 500) у свої помилки і на рівні middleware мапте їх у відповіді.

  • Гарантуйте instanceof. Сучасні рушії з цим у порядку, але шаблон extends Error разом із captureStackTrace (якщо він доступний), це надійна база. Якщо ви компілюєте в ES5, додатково потрібен Object.setPrototypeOf(this, new.target.prototype).

Типові помилки

  • Забути super(message). Без нього message буде порожнім, а стек, обрізаним.
  • Не виставити name. У логах і в моніторингу помилка з'явиться як звичайний Error, і відфільтрувати її буде нічим.
  • Втратити оригінальну помилку. Перехопили низькорівневий виняток і кинули новий без cause, отже причину збою вже не відновити.
  • Покладатися на e.constructor.name замість instanceof. Після мініфікації імена класів змінюються, а instanceof працює далі.
  • Розраховувати на Error.captureStackTrace скрізь. Це специфіка V8, тому виклик обов'язково треба обгорнути перевіркою наявності.
  • Класти в помилку чутливі дані. Об'єкт помилки часто повністю потрапляє в логи, тому паролям і токенам у details не місце.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.