Як створити власний тип помилки
Власний тип помилки, це звичайний клас, що успадковує 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.
Швидкий приклад
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+)
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;
}
}Використання:
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
Це корисно, коли ви перехоплюєте низькорівневий виняток і хочете додати контекст, не втративши оригінал:
try {
JSON.parse("not valid JSON");
} catch (e) {
throw new AppError("Failed to parse the config", { cause: e, code: "CONFIG_PARSE" });
}Пізніше можна дивитися весь ланцюжок:
console.error(err.cause); // вихідна помилкаВерсія для 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):
javascriptfunction 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не місце.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.