Пошук уроків, статей та іншого контенту
Розберете повторне викидання помилок і побудову ланцюжка обробників під час передавання помилок між функціями.
Якщо під час виконання функції виникає помилка, JavaScript шукає найближчий блок catch, який може її обробити.
Пошук відбувається вгору за стеком викликів:
помилка виникає у вкладеній функції;
якщо в цій функції немає відповідного try...catch, помилка передається функції, яка її викликала;
пошук триває далі;
якщо обробника немає в усьому ланцюжку, програма завершує поточну операцію з необробленою помилкою.
function readConfig() {
throw new Error("Файл конфігурації не знайдено");
}
function startApplication() {
readConfig();
console.log("Застосунок запущено");
}
try {
startApplication();
} catch (error) {
console.error("Не вдалося запустити застосунок:", error.message);
}У цьому прикладі readConfig не має власного обробника. Помилка поширюється до startApplication, а потім — до зовнішнього try...catch.
Результат:
Не вдалося запустити застосунок: Файл конфігурації не знайденоРядок Застосунок запущено не виконується, тому що після виникнення помилки нормальний потік виконання функції переривається.
Блок catch перехоплює помилку та отримує її об’єкт:
try {
JSON.parse("{invalid json}");
} catch (error) {
console.error(error.name);
console.error(error.message);
}Зазвичай об’єкт помилки має такі властивості:
name — назва типу помилки;
message — повідомлення;
stack — стек викликів, корисний для діагностики.
Перехоплення помилки не означає, що її обов’язково потрібно повністю обробити на цьому рівні. Іноді функція може виконати локальні дії, а потім передати помилку далі.
Повторне викидання, або rethrow, — це передавання перехопленої помилки далі за допомогою throw.
function parseUser(input) {
try {
return JSON.parse(input);
} catch (error) {
console.log("Не вдалося розібрати JSON");
throw error;
}
}
try {
parseUser("{invalid json}");
} catch (error) {
console.error("Помилка на верхньому рівні:", error.message);
}У цьому прикладі parseUser:
перехоплює помилку;
виконує локальну дію — записує повідомлення в журнал;
повторно викидає ту саму помилку.
Важливо, що після throw error виконання функції припиняється. Код після цього рядка не виконається.
function processData() {
try {
throw new Error("Помилка обробки");
} catch (error) {
console.log("Помилку перехоплено");
throw error;
// Цей рядок недосяжний
console.log("Цей текст не виведеться");
}
}У реальних застосунках різні рівні можуть відповідати за різні завдання:
низькорівнева функція працює з файлом або мережею;
сервісна функція додає бізнес-контекст;
верхній рівень показує користувачу зрозуміле повідомлення;
глобальний обробник записує непередбачені помилки.
Розглянемо приклад із кількома рівнями:
function loadUserFromDatabase(userId) {
if (userId <= 0) {
throw new RangeError("Ідентифікатор користувача має бути додатним");
}
// Імітація помилки під час роботи з базою даних
throw new Error("З'єднання з базою даних недоступне");
}
function getUserProfile(userId) {
try {
return loadUserFromDatabase(userId);
} catch (error) {
// Додаємо контекст і передаємо помилку далі
throw new Error(
`Не вдалося завантажити профіль користувача ${userId}`,
{ cause: error }
);
}
}
function showUserProfile(userId) {
try {
const profile = getUserProfile(userId);
console.log(profile);
} catch (error) {
console.error(error.message);
if (error.cause) {
console.error("Початкова причина:", error.cause.message);
}
}
}
showUserProfile(42);Результат:
Не вдалося завантажити профіль користувача 42
Початкова причина: З'єднання з базою даних недоступнеВластивість cause дає змогу додати до нової помилки початкову причину. Це краще, ніж просто створити нову помилку та втратити інформацію про джерело проблеми.
Повторно викидайте помилку, якщо поточна функція:
не може повністю вирішити проблему;
має додати контекст;
повинна виконати локальне логування;
повинна перетворити технічну помилку на помилку вищого рівня;
повинна передати керування спеціалізованому обробнику.
function saveOrder(order) {
try {
if (!order.id) {
throw new Error("У замовлення немає ідентифікатора");
}
// Збереження замовлення
} catch (error) {
throw new Error("Помилка збереження замовлення", {
cause: error
});
}
}Верхній рівень може показати загальне повідомлення, а cause використати для журналювання або діагностики.
Не кожну помилку потрібно обробляти однаково. Можна перевіряти тип помилки за допомогою instanceof.
function validateAge(age) {
if (typeof age !== "number") {
throw new TypeError("Вік має бути числом");
}
if (age < 0) {
throw new RangeError("Вік не може бути від’ємним");
}
return true;
}
function registerUser(age) {
try {
validateAge(age);
console.log("Користувача зареєстровано");
} catch (error) {
if (error instanceof RangeError) {
console.error("Помилка значення:", error.message);
return;
}
// Невідомі помилки не приховуємо
throw error;
}
}
registerUser(-2);У цьому прикладі RangeError обробляється локально. Інші помилки повторно викидаються, тому що функція registerUser не знає, як їх коректно обробити.
Такий підхід безпечніший за порожній catch, який приховує всі проблеми без винятку.
Іноді внутрішні деталі не повинні виходити за межі певного шару програми. Наприклад, код роботи з базою даних може викидати технічні помилки, а сервіс може перетворювати їх на помилку доменного рівня.
class UserNotFoundError extends Error {
constructor(userId, options = {}) {
super(`Користувача з ідентифікатором ${userId} не знайдено`, options);
this.name = "UserNotFoundError";
this.userId = userId;
}
}
function findUserInStorage(userId) {
throw new Error("Помилка запиту до сховища");
}
function findUser(userId) {
try {
return findUserInStorage(userId);
} catch (error) {
throw new UserNotFoundError(userId, { cause: error });
}
}
try {
findUser(10);
} catch (error) {
if (error instanceof UserNotFoundError) {
console.error("Користувача не знайдено");
console.error("Технічна причина:", error.cause.message);
} else {
throw error;
}
}Тут зовнішній код працює з UserNotFoundError, а початкова технічна помилка залишається доступною через cause.
Під час перетворення помилки важливо:
не втрачати початкову причину;
використовувати зрозумілий тип помилки;
не показувати користувачу внутрішні технічні деталі;
зберігати повну інформацію для журналу.
finally у ланцюжку обробкиБлок finally виконується незалежно від того, виникла помилка чи ні. Його використовують для звільнення ресурсів, закриття з’єднань або завершення службових дій.
function readFile() {
console.log("Відкриття файлу");
try {
throw new Error("Помилка читання файлу");
} finally {
console.log("Файл закрито");
}
}
try {
readFile();
} catch (error) {
console.error("Обробка помилки:", error.message);
}Результат:
Відкриття файлу
Файл закрито
Обробка помилки: Помилка читання файлуfinally виконається до того, як помилка продовжить поширюватися до зовнішнього catch.
Не варто використовувати return у finally, якщо потрібно зберегти помилку:
function dangerousFunction() {
try {
throw new Error("Важлива помилка");
} finally {
return "Успішне завершення";
}
}
console.log(dangerousFunction());У цьому випадку return у finally придушує помилку. Функція поверне рядок, а помилка буде втрачена. Це майже завжди небажана поведінка.
Для функцій, які повертають Promise, помилка поширюється як відхилення проміса. Її можна обробити через try...catch в async-функції.
function fetchUser() {
return Promise.reject(new Error("Сервер недоступний"));
}
async function loadProfile() {
try {
return await fetchUser();
} catch (error) {
throw new Error("Не вдалося завантажити профіль", {
cause: error
});
}
}
async function main() {
try {
await loadProfile();
} catch (error) {
console.error(error.message);
console.error("Причина:", error.cause.message);
}
}
main();await важливий у try, оскільки він очікує завершення проміса та перетворює його відхилення на виняток, який можна перехопити.
Без await try...catch не перехопить відхилення проміса:
async function incorrectExample() {
try {
fetchUser();
} catch (error) {
// Цей catch не перехопить відхилення проміса
console.error(error.message);
}
}Правильні варіанти:
async function correctExample() {
try {
await fetchUser();
} catch (error) {
console.error(error.message);
}
}Або передавання проміса далі:
function getData() {
return fetchUser().catch((error) => {
throw new Error("Помилка отримання даних", {
cause: error
});
});
}Логувати одну й ту саму помилку на кожному рівні зазвичай не потрібно. Це створює дублікати в журналах і ускладнює пошук проблеми.
Краще розподілити відповідальність:
нижній рівень додає технічний контекст;
середній рівень перетворює помилку або повторно її викидає;
верхній рівень один раз записує повну помилку;
рівень інтерфейсу показує користувачу безпечне повідомлення.
function handleRequest() {
try {
throw new Error("Внутрішня помилка сервера");
} catch (error) {
console.error("Повний журнал:", error);
console.error("Повідомлення для користувача: Спробуйте пізніше");
}
}
handleRequest();Користувачу не слід показувати stack, SQL-запити, внутрішні шляхи до файлів або службові ключі.
catchtry {
doSomething();
} catch (error) {
// Нічого не робимо
}Такий код приховує проблему. Якщо помилку справді потрібно ігнорувати, це має бути свідоме рішення з поясненням:
try {
loadOptionalSettings();
} catch (error) {
// Додаткові налаштування необов'язкові, використовуємо значення за замовчуванням
}try {
readData();
} catch (error) {
throw new Error("Не вдалося прочитати дані");
}Початкова причина втрачається. Краще використати cause:
try {
readData();
} catch (error) {
throw new Error("Не вдалося прочитати дані", {
cause: error
});
}Технічно JavaScript дозволяє викидати будь-яке значення:
throw "Помилка";
throw 404;
throw null;Але це ускладнює обробку, оскільки значення не має стандартних властивостей помилки. Переважно потрібно викидати екземпляри Error або його підтипів:
throw new Error("Опис помилки");
throw new TypeError("Неправильний тип даних");try {
validateInput();
} catch (error) {
console.log("Некоректні дані");
}Так можна випадково приховати помилку програмування або несправність зовнішнього сервісу. Якщо функція може обробити лише певний тип помилки, інші потрібно повторно викинути.
finallyНе повертайте значення з finally, якщо перед цим могла виникнути помилка. return у finally може замінити результат try або catch і придушити виняток.
Під час проєктування ланцюжка обробки помилок поставте такі запитання:
На якому рівні помилка може бути виправлена?
Який контекст потрібно додати?
Чи потрібно перетворити тип помилки?
Хто має остаточно повідомити про помилку?
Чи зберігається початкова причина через cause?
Чи не буде помилка залогована кілька разів?
Чи звільняються ресурси в finally?
Чи очікується асинхронна операція через await?
Якщо поточний рівень не може коректно обробити помилку, не потрібно робити вигляд, що проблема вирішена. Додайте потрібний контекст і передайте помилку далі.
Помилка автоматично поширюється вгору стеком викликів, якщо не знайдено локальний обробник.
catch може обробити помилку або виконати локальні дії перед повторним викиданням.
Повторне викидання виконується оператором throw.
Для перетворення помилки на новий тип використовуйте cause, щоб не втратити початкову причину.
Обробляйте лише ті типи помилок, які поточний рівень справді може вирішити.
finally призначений для гарантованого очищення ресурсів.
В асинхронному коді використовуйте await усередині try, щоб перехоплювати відхилення промісів.
Не приховуйте помилки порожнім catch, не викидайте довільні значення та не використовуйте return у finally без особливої потреби.