Розширення 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.
Швидкий приклад
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.
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-шару зручно мати статус і структуровані деталі прямо в помилці:
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 }
}З кількох таких класів будують ціле «дерево» помилок:
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;
}
}Використання:
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, коли обгортаєте низькорівневу помилку у свою доменну.
try {
JSON.parse("невалідний JSON");
} catch (parseErr) {
throw new AppError("Помилка розбору JSON", "PARSE_ERROR", { cause: parseErr });
}Щоб це працювало, третій аргумент треба прокинути в super:
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
Головна вигода власних класів у тому, що один обробник вміє коректно відповісти на будь-яку з ваших помилок і не розкриває внутрішні подробиці для чужих:
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 |
Підсумковий шаблон:
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, тому виклик робіть опціональним.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.