Skip to main content

Розширення Error власними властивостями

Розширити Error означає оголосити власний клас, що наслідує Error, передати повідомлення батьківському конструктору через super(message) і додати до нього свої властивості. Далі такі помилки можна кидати як звичайні, але в catch у вас з'являється не просто текст, а структура: код, HTTP-статус, деталі та причина.

Теорія

TL;DR

  • Власний клас помилки: class AppError extends Error, у конструкторі обов'язково super(message).
  • Після super дописуйте будь-які поля: code, status, details, context.
  • this.name = this.constructor.name робить ім'я помилки читабельним у логах і в stack trace.
  • Error.captureStackTrace(this, this.constructor) (V8) прибирає власний конструктор зі stack trace.
  • З ES2022 є стандартне поле cause, яке зберігає початкову помилку: new Error(msg, { cause: err }).
  • Ієрархія класів (AppError -> ValidationError, AuthError) дає зручні перевірки через instanceof.

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

javascript
class AppError extends Error { constructor(message, code) { super(message); // передаємо повідомлення батьківському Error this.name = "AppError"; // задаємо ім'я помилки this.code = code; // додаємо власну властивість } } try { throw new AppError("Щось пішло не так", "SERVER_ERROR"); } catch (e) { console.log(e.name); // AppError console.log(e.message); // Щось пішло не так console.log(e.code); // SERVER_ERROR console.log(e.stack); // стек викликів }

Тепер у вашої помилки є і стандартні властивості (message, stack), і власні (code).

Базовий клас і stack trace

Мінімальний контракт власної помилки складається з трьох речей: виклик super(message), коректне name і чистий stack trace.

javascript
class AppError extends Error { constructor(message, code) { super(message); this.name = this.constructor.name; // ім'я береться з класу, а не з рядка this.code = code; Error.captureStackTrace?.(this, this.constructor); } }
  • super(message) обов'язковий: без нього this недоступний, а message і stack не будуть заповнені.
  • this.name = this.constructor.name автоматично дає "AppError", "ValidationError" тощо, тож не треба дублювати рядок у кожному підкласі.
  • Error.captureStackTrace існує у V8 (Node.js, Chrome). Другий аргумент виключає зі стека кадр самого конструктора, тож перший рядок вказує на місце, де помилку справді кинули. Опціональний виклик ?.() робить код безпечним у рушіях, де цього методу немає.

Статус, деталі та ієрархія помилок

Для HTTP-шару зручно мати статус і структуровані деталі прямо в помилці:

javascript
class HttpError extends Error { constructor(message, status = 500, details = {}) { super(message); this.name = this.constructor.name; this.status = status; this.details = details; if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } } try { throw new HttpError("Користувача не знайдено", 404, { userId: 123 }); } catch (err) { console.log(err.name); // HttpError console.log(err.status); // 404 console.log(err.details); // { userId: 123 } }

З кількох таких класів будують ціле «дерево» помилок:

javascript
class AppError extends Error { constructor(message, code) { super(message); this.name = this.constructor.name; this.code = code; Error.captureStackTrace?.(this, this.constructor); } } class ValidationError extends AppError { constructor(message, field) { super(message, "VALIDATION_ERROR"); this.field = field; } } class AuthError extends AppError { constructor(message = "Unauthorized") { super(message, "AUTH_ERROR"); this.status = 401; } }

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

javascript
try { throw new ValidationError("Некоректний email", "email"); } catch (err) { if (err instanceof ValidationError) { console.log("Помилка в полі:", err.field); } console.log(err.code); // VALIDATION_ERROR }

Оскільки ValidationError наслідує AppError, перевірка err instanceof AppError теж істинна. Це дає два рівні обробки: загальний для всіх своїх помилок і точковий для конкретного типу.

Властивість cause (ES2022)

Сучасний стандарт ECMAScript підтримує властивість cause, щоб зберігати вихідну помилку, через яку виникла поточна. Так ви не втрачаєте початковий stack trace, коли обгортаєте низькорівневу помилку у свою доменну.

javascript
try { JSON.parse("невалідний JSON"); } catch (parseErr) { throw new AppError("Помилка розбору JSON", "PARSE_ERROR", { cause: parseErr }); }

Щоб це працювало, третій аргумент треба прокинути в super:

javascript
class AppError extends Error { constructor(message, code, options = {}) { super(message, options); // рушій сам виставить this.cause this.name = this.constructor.name; this.code = code; this.cause = options.cause; // сумісність зі старішими рушіями } }

Централізована обробка в Express або NestJS

Головна вигода власних класів у тому, що один обробник вміє коректно відповісти на будь-яку з ваших помилок і не розкриває внутрішні подробиці для чужих:

javascript
app.use((err, req, res, next) => { if (err instanceof AppError) { res.status(err.status || 500).json({ error: err.name, message: err.message, code: err.code, details: err.details, }); } else { res.status(500).json({ error: "InternalError", message: "Невідома помилка" }); } });

Навіщо розширювати Error

ПричинаПриклад
Чіткі та зрозумілі повідомлення"Email є обов'язковим" замість "Unexpected token"
Структура і типізаціяValidationError, AuthError, HttpError
Зручність логуваннядо логу потрапляють code, status, details
Збереження початкової причиничерез cause
Зручність налагодженнярізні типи помилок легко розрізнити в catch

Підсумковий шаблон:

javascript
class MyError extends Error { constructor(message, customProp) { super(message); this.name = "MyError"; this.customProp = customProp; Error.captureStackTrace?.(this, this.constructor); } } throw new MyError("Помилка бази даних", { query: "SELECT * FROM users" });

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

  • Забути super(message). Конструктор кине ReferenceError на першому ж звертанні до this, а якщо викликати super() без аргументу, то message залишиться порожнім.
  • Не виставити name. Тоді в логах і в stack буде просто Error, і власний тип стає невидимим.
  • Покладатися на this.constructor.name після мінізації. Бандлер може перейменувати клас, тому для публічних кодів помилок краще окреме поле code, а не name.
  • Перевіряти тип через err.name === "AppError" замість instanceof. Рядок легко зламати, до того ж instanceof бачить усю ієрархію.
  • Транспілювати в ES5 без налаштувань. При таргеті ES5 class X extends Error ламає прототипний ланцюжок, і instanceof перестає працювати. Лікується рядком Object.setPrototypeOf(this, new.target.prototype) у конструкторі або таргетом ES2015+.
  • Класти в помилку чутливі дані. details часто йде в лог і в HTTP-відповідь, тому паролі, токени та персональні дані туди потрапляти не мають.
  • Розраховувати на Error.captureStackTrace всюди. Це нестандартне розширення V8, тому виклик робіть опціональним.

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

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

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