Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Розширення Error власними властивостями». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Щоб розширити `Error`, створюють власний клас-нащадок, у конструкторі викликають `super(message)` і дописують потрібні поля: `code`, `status`, `details`, `cause`, `context`.** Такий об'єкт зберігає стандартні `message` та `stack`, коректно працює з `instanceof` і дає змогу централізовано розрізняти типи помилок у `catch` чи в middleware. ```javascript class AppError extends Error { constructor(message, code) { super(message); this.name = "AppError"; this.code = code; Error.captureStackTrace?.(this, this.constructor); } } ``` **Ключове:** `super(message)` дає стандартну частину помилки, власні поля дають структуру, а `Error.captureStackTrace` прибирає конструктор зі stack trace.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Розширити `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, тому виклик робіть опціональним.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.