Пошук уроків, статей та іншого контенту
Створите власні класи помилок для розділення типів проблем і обробки кожного випадку відповідно до його призначення.
Стандартний клас Error повідомляє, що під час виконання сталася помилка, але часто цього недостатньо. У реальному застосунку різні проблеми потрібно обробляти по-різному:
користувач ввів некоректні дані;
запитаний ресурс не знайдено;
користувач не має необхідних прав;
відбулася помилка підключення до сервісу;
сталася непередбачена внутрішня помилка.
Користувацькі класи помилок дають змогу створити окремий тип для кожної категорії проблем. Тоді код може перевіряти не лише текст повідомлення, а й тип помилки.
try {
// Виконуємо операцію
} catch (error) {
if (error instanceof ValidationError) {
// Обробка помилки валідації
} else if (error instanceof NotFoundError) {
// Обробка відсутнього ресурсу
} else {
// Обробка невідомої помилки
}
}Такий підхід надійніший за перевірку рядків:
if (error.message === "Користувача не знайдено") {
// Цей варіант крихкий:
// текст повідомлення може змінитися
}ErrorКористувацький клас помилки створюють за допомогою extends Error.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
const error = new ValidationError("Email має некоректний формат");
console.log(error instanceof ValidationError); // true
console.log(error instanceof Error); // true
console.log(error.name); // ValidationError
console.log(error.message); // Email має некоректний форматКлас ValidationError успадковує від Error:
властивість message;
властивість stack;
поведінку стандартної помилки;
можливість перевірки через instanceof.
Виклик super(message) обов’язковий до використання this у конструкторі дочірнього класу. Він викликає конструктор базового класу Error.
nameЗа замовчуванням у дочірнього класу може залишатися ім’я Error. Щоб у трасуванні та логах було видно конкретний тип помилки, зазвичай задають this.name:
class PermissionError extends Error {
constructor(message) {
super(message);
this.name = "PermissionError";
}
}
try {
throw new PermissionError("Недостатньо прав");
} catch (error) {
console.log(error.name); // PermissionError
console.log(error.message); // Недостатньо прав
console.log(error.stack); // Стек викликів
}Кожен клас може описувати окрему ситуацію.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource, identifier) {
super(`${resource} з ідентифікатором "${identifier}" не знайдено`);
this.name = "NotFoundError";
this.resource = resource;
this.identifier = identifier;
}
}
class AuthenticationError extends Error {
constructor(message = "Користувач не автентифікований") {
super(message);
this.name = "AuthenticationError";
}
}Тепер помилка може містити не лише текст, а й додаткові дані:
const validationError = new ValidationError(
"Пароль має містити щонайменше 8 символів",
"password"
);
console.log(validationError.field); // password
const notFoundError = new NotFoundError("Користувача", 42);
console.log(notFoundError.message);
// Користувача з ідентифікатором "42" не знайдено
console.log(notFoundError.resource); // Користувача
console.log(notFoundError.identifier); // 42Ці властивості можуть бути корисними під час формування відповіді API, запису журналів або показу повідомлення у формі.
instanceofОператор instanceof перевіряє, чи належить об’єкт до певного класу або класу, від якого він успадкований.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource, identifier) {
super(`${resource} з ідентифікатором "${identifier}" не знайдено`);
this.name = "NotFoundError";
this.resource = resource;
this.identifier = identifier;
}
}
function getUser(id) {
if (typeof id !== "number" || id <= 0) {
throw new ValidationError(
"Ідентифікатор має бути додатним числом",
"id"
);
}
if (id !== 1) {
throw new NotFoundError("Користувач", id);
}
return {
id: 1,
name: "Олена"
};
}
function showUser(id) {
try {
const user = getUser(id);
console.log(`Користувач: ${user.name}`);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Помилка у полі "${error.field}": ${error.message}`);
} else if (error instanceof NotFoundError) {
console.log(`Ресурс не знайдено: ${error.message}`);
} else {
console.error("Невідома помилка:", error);
}
}
}
showUser("1");
// Помилка у полі "id": Ідентифікатор має бути додатним числом
showUser(10);
// Ресурс не знайдено: Користувач з ідентифікатором "10" не знайдено
showUser(1);
// Користувач: ОленаПеревіряти спеціалізовані типи потрібно до загального Error. Оскільки ValidationError є також екземпляром Error, така послідовність важлива:
try {
// ...
} catch (error) {
if (error instanceof ValidationError) {
// Спеціальна обробка
} else if (error instanceof Error) {
// Загальна обробка
}
}Якщо спочатку перевірити Error, до спеціальної гілки виконання вже не дійде.
У великих програмах зручно створити базовий клас для помилок застосунку, а від нього успадковувати конкретні типи.
class AppError extends Error {
constructor(message, options = {}) {
super(message);
this.name = this.constructor.name;
this.code = options.code;
this.statusCode = options.statusCode ?? 500;
}
}
class ValidationError extends AppError {
constructor(message, field) {
super(message, {
code: "VALIDATION_ERROR",
statusCode: 400
});
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource, identifier) {
super(`${resource} з ідентифікатором "${identifier}" не знайдено`, {
code: "NOT_FOUND",
statusCode: 404
});
this.resource = resource;
this.identifier = identifier;
}
}
const error = new ValidationError("Email є обов’язковим", "email");
console.log(error.name); // ValidationError
console.log(error.code); // VALIDATION_ERROR
console.log(error.statusCode); // 400
console.log(error instanceof AppError); // true
console.log(error instanceof Error); // trueБазовий клас може містити спільні властивості:
code — стабільний програмний код помилки;
statusCode — HTTP-статус для серверного застосунку;
name — назва класу;
додаткові поля, спільні для всіх помилок.
Назва класу не завжди підходить як програмний код. Наприклад, назву класу можна перейменувати, а значення code залишити незмінним для клієнтського коду.
На внутрішньому рівні функції можуть створювати спеціалізовані помилки. На зовнішньому рівні їх можна перетворити на зрозумілу відповідь.
class AppError extends Error {
constructor(message, { code, statusCode = 500 } = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
}
}
class ValidationError extends AppError {
constructor(message, field) {
super(message, {
code: "VALIDATION_ERROR",
statusCode: 400
});
this.field = field;
}
}
class NotFoundError extends AppError {
constructor(resource, identifier) {
super(`${resource} не знайдено`, {
code: "NOT_FOUND",
statusCode: 404
});
this.resource = resource;
this.identifier = identifier;
}
}
function findProduct(id) {
if (!Number.isInteger(id) || id <= 0) {
throw new ValidationError(
"Ідентифікатор товару має бути додатним цілим числом",
"id"
);
}
if (id !== 1) {
throw new NotFoundError("Товар", id);
}
return {
id: 1,
name: "Клавіатура"
};
}
function handleRequest(id) {
try {
const product = findProduct(id);
return {
statusCode: 200,
body: {
data: product
}
};
} catch (error) {
if (error instanceof AppError) {
return {
statusCode: error.statusCode,
body: {
error: {
code: error.code,
message: error.message,
...(error.field && { field: error.field })
}
}
};
}
console.error(error);
return {
statusCode: 500,
body: {
error: {
code: "INTERNAL_ERROR",
message: "Внутрішня помилка сервера"
}
}
};
}
}
console.log(handleRequest("1"));
console.log(handleRequest(20));
console.log(handleRequest(1));Окремо важливо приховувати внутрішні деталі непередбачених помилок. Стек викликів і технічний текст можуть містити службову інформацію, яку не слід надсилати користувачу.
causeІноді одна помилка виникає через іншу. Наприклад, сервіс не зміг завантажити дані, тому що мережевий запит завершився помилкою.
Для збереження першопричини можна використовувати властивість cause.
class DataLoadError extends Error {
constructor(message, cause) {
super(message, { cause });
this.name = "DataLoadError";
}
}
function loadData() {
try {
throw new Error("З'єднання з базою даних закрито");
} catch (error) {
throw new DataLoadError("Не вдалося завантажити дані", error);
}
}
try {
loadData();
} catch (error) {
console.log(error.name); // DataLoadError
console.log(error.message); // Не вдалося завантажити дані
console.log(error.cause.message); // З'єднання з базою даних закрито
}Зовнішня помилка описує проблему на рівні поточної операції, а cause зберігає технічну причину. Це зручно для журналювання:
try {
loadData();
} catch (error) {
console.error("Основна помилка:", error.message);
console.error("Причина:", error.cause);
}Не обов’язково передавати внутрішню причину користувачу. Її зазвичай використовують для діагностики.
Властивості помилки не завжди повністю потрапляють до результату JSON.stringify, оскільки стандартні властивості Error, зокрема message і stack, є неперелічуваними.
const error = new Error("Сталася помилка");
console.log(JSON.stringify(error)); // {}Якщо потрібно сформувати JSON, створіть об’єкт явно або додайте метод перетворення:
class AppError extends Error {
constructor(message, { code, statusCode = 500 } = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
}
toJSON() {
return {
code: this.code,
message: this.message,
statusCode: this.statusCode
};
}
}
const error = new AppError("Ресурс недоступний", {
code: "RESOURCE_UNAVAILABLE",
statusCode: 503
});
console.log(JSON.stringify(error));
// {"code":"RESOURCE_UNAVAILABLE","message":"Ресурс недоступний","statusCode":503}Метод toJSON корисний, коли помилка передається через API або записується у структурований журнал.
Користувацькі класи помилок працюють однаково з синхронними та асинхронними функціями. Для Promise помилку потрібно перехоплювати через try...catch навколо await або через .catch().
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
async function createUser(data) {
if (!data.email) {
throw new ValidationError("Email є обов’язковим", "email");
}
return {
id: 1,
email: data.email
};
}
async function main() {
try {
const user = await createUser({});
console.log(user);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Поле "${error.field}": ${error.message}`);
} else {
console.error("Невідома помилка:", error);
}
}
}
main();Якщо не використати await у try, помилка з асинхронної функції може не потрапити до цього обробника:
try {
createUser({});
} catch (error) {
// Цей блок не перехопить помилку з Promise
}Правильний варіант:
try {
await createUser({});
} catch (error) {
// Помилка буде перехоплена
}Або:
createUser({}).catch((error) => {
console.error(error);
});Користувацький клас виправданий, якщо код має виконувати різні дії для різних категорій проблем.
Добрі приклади:
помилки валідації;
помилки автентифікації та авторизації;
відсутність ресурсу;
конфлікт під час зміни даних;
помилки зовнішнього сервісу;
помилки конфігурації;
помилки доступу до бази даних.
Не потрібно створювати окремий клас для кожного повідомлення. Якщо дві ситуації обробляються однаково, їм часто достатньо одного класу з різними властивостями.
Наприклад, усі помилки валідації можуть бути екземплярами ValidationError, а конкретне поле передаватися через field.
ErrorНеправильно створювати помилку як звичайний клас без наслідування:
class ValidationError {
constructor(message) {
this.message = message;
}
}Такий об’єкт не має повної поведінки Error і не буде екземпляром Error.
Правильний варіант:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}superУ дочірньому конструкторі потрібно викликати super:
class ValidationError extends Error {
constructor(message) {
super(message);
}
}Без цього JavaScript не дозволить коректно створити екземпляр дочірнього класу.
error.nameПеревірка через name можлива, але вона менш надійна:
if (error.name === "ValidationError") {
// ...
}Значення name можна змінити вручну. Якщо класи доступні в поточному модулі, краще використовувати:
if (error instanceof ValidationError) {
// ...
}Error перед спеціальними класамиНеправильно:
if (error instanceof Error) {
// Ця гілка перехопить і ValidationError
} else if (error instanceof ValidationError) {
// Сюди виконання не дійде
}Правильно спочатку перевіряти спеціалізовані класи, а потім базові:
if (error instanceof ValidationError) {
// Спеціальна обробка
} else if (error instanceof Error) {
// Загальна обробка
}Повідомлення призначене для людей і може змінюватися. Для програмної логіки використовуйте:
клас помилки;
стабільний code;
додаткові властивості, наприклад field або statusCode.
Не слід показувати користувачу stack, SQL-запити, внутрішні шляхи до файлів або секретні дані. Технічну інформацію потрібно залишати для журналів, а назовні повертати безпечне повідомлення.
Користувацькі помилки створюють через class ... extends Error.
У конструкторі потрібно викликати super(message).
Властивість name допомагає ідентифікувати тип помилки в логах.
instanceof дає змогу обробляти різні класи помилок окремо.
Спочатку перевіряйте спеціалізовані помилки, а потім загальний Error.
Базовий клас на кшталт AppError може містити спільні властивості: code, statusCode та інші метадані.
Властивість cause зберігає першопричину помилки.
Для асинхронного коду помилки потрібно перехоплювати навколо await або через .catch().
Не використовуйте текст повідомлення як основний спосіб визначення типу проблеми.
Внутрішні технічні деталі помилок не слід безпосередньо показувати користувачу.