Пошук уроків, статей та іншого контенту
Навчимося перехоплювати, поширювати й класифікувати помилки в Promise та async/await коді.
У синхронному коді помилку можна перехопити через try...catch:
try {
throw new Error("Щось пішло не так");
} catch (error) {
console.error(error.message);
}В асинхронному коді помилки зазвичай представлені як відхилені Promise (rejected Promise).
async function loadData() {
throw new Error("Не вдалося завантажити дані");
}
loadData().catch((error) => {
console.error(error.message);
});Функція, оголошена з async, завжди повертає Promise. Якщо всередині неї виникає помилка або виконується throw, цей Promise відхиляється.
.catch()Для Promise помилки перехоплюють методом .catch():
function getUser() {
return Promise.reject(new Error("Користувача не знайдено"));
}
getUser()
.then((user) => {
console.log(user);
})
.catch((error) => {
console.error("Помилка:", error.message);
});.catch() перехоплює:
Promise, відхилений через reject;
помилку, викинуту через throw у попередньому обробнику .then();
помилку, викинуту всередині функції, позначеної async.
Promise.resolve()
.then(() => {
throw new Error("Помилка в then");
})
.then(() => {
console.log("Цей обробник не виконається");
})
.catch((error) => {
console.error(error.message);
});Після помилки виконання переходить до найближчого наступного .catch() у ланцюжку.
Якщо .catch() не обробляє помилку остаточно, він може повторно викинути її за допомогою throw:
getUser()
.catch((error) => {
console.error("Локальне логування:", error.message);
throw error;
})
.then((user) => {
console.log(user);
})
.catch((error) => {
console.error("Фінальна обробка:", error.message);
});Це корисно, коли нижчий рівень:
записує технічну інформацію в лог;
додає контекст;
передає помилку вище для остаточного рішення.
Якщо в обробнику .catch() нічого не повернути і не викинути нову помилку, Promise вважатиметься успішно обробленим.
Promise.reject(new Error("Помилка"))
.catch((error) => {
console.error(error.message);
// Помилка вважається обробленою
})
.then(() => {
console.log("Цей код виконається");
});async/awaitasync/await дає змогу працювати з Promise у стилі синхронного коду. Для цього використовується try...catch:
async function run() {
try {
const result = await Promise.reject(new Error("Помилка запиту"));
console.log(result);
} catch (error) {
console.error("Запит завершився помилкою:", error.message);
}
}
run();await призупиняє виконання функції до завершення Promise. Якщо Promise відхилено, await викидає його помилку, тому її можна перехопити через catch.
try...catchУ try потрібно поміщати саме операції, які можуть завершитися помилкою:
async function loadUser() {
try {
const response = await requestUser();
return response;
} catch (error) {
console.error("Не вдалося завантажити користувача");
throw error;
}
}Якщо виклик асинхронної функції винести за межі try, помилка не буде перехоплена цим блоком:
async function loadUser() {
const responsePromise = requestUser();
try {
return await responsePromise;
} catch (error) {
console.error("Помилка була перехоплена");
}
}У цьому прикладі Promise все ще очікується всередині try, тому помилка буде перехоплена. Але якщо Promise не очікувати і не повернути його в ланцюжок, try...catch не допоможе:
async function loadUser() {
try {
requestUser().then((response) => {
console.log(response);
});
} catch (error) {
// Цей блок не перехопить асинхронну помилку
console.error(error);
}
}Правильні варіанти:
async function loadUser() {
try {
const response = await requestUser();
console.log(response);
} catch (error) {
console.error(error);
}
}або:
function loadUser() {
return requestUser()
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
});
}Не кожна функція повинна остаточно обробляти помилку. Часто нижчий рівень лише передає її вище.
async function readSettings() {
try {
return await loadSettingsFromFile();
} catch (error) {
console.error("Помилка читання налаштувань");
throw error;
}
}
async function startApplication() {
try {
const settings = await readSettings();
console.log("Застосунок запущено:", settings);
} catch (error) {
console.error("Застосунок не можна запустити:", error.message);
}
}Тут:
readSettings() знає, де сталася помилка;
startApplication() знає, що робити, якщо налаштування не завантажилися;
помилка не втрачається між рівнями.
Верхній рівень програми зазвичай має вирішити, як повідомити користувача, повернути HTTP-відповідь або завершити операцію.
finally для завершальних дійБлок finally виконується незалежно від результату:
якщо операція завершилася успішно;
якщо виникла помилка;
якщо помилку повторно викинули через throw.
async function processFile() {
let isLocked = false;
try {
isLocked = true;
await saveFile();
} catch (error) {
console.error("Не вдалося зберегти файл:", error.message);
throw error;
} finally {
isLocked = false;
console.log("Ресурс звільнено");
}
}finally часто використовують для:
звільнення блокування;
закриття з’єднання;
очищення тимчасових даних;
зупинки індикатора завантаження.
Не варто повертати значення з finally, якщо потрібно зберегти початковий результат або помилку:
async function example() {
try {
throw new Error("Початкова помилка");
} finally {
return "Успіх";
}
}
example().then((result) => {
console.log(result); // "Успіх"
});return у finally приховав початкову помилку. Це майже завжди небажано.
Різні помилки потребують різної реакції. Наприклад:
помилка валідації — потрібно виправити вхідні дані;
ресурс не знайдено — повернути відповідь 404;
помилка зовнішнього сервісу — повторити операцію або повідомити про тимчасову проблему;
невідома помилка — записати деталі та не маскувати її.
Для цього створюють власні класи помилок:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "NotFoundError";
}
}Перевірити тип помилки можна через instanceof:
try {
throw new ValidationError("Некоректний ідентифікатор");
} catch (error) {
if (error instanceof ValidationError) {
console.error("Помилка вхідних даних:", error.message);
} else {
throw error;
}
}Невідомі помилки краще передавати далі, а не перетворювати на загальне повідомлення без причини.
Нижче функція loadUser імітує асинхронне звернення до сховища. Функція getUserProfile додає бізнес-логіку, а main класифікує помилки на верхньому рівні.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "NotFoundError";
}
}
class UserServiceError extends Error {
constructor(message, cause) {
super(message);
this.name = "UserServiceError";
this.cause = cause;
}
}
function loadUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!Number.isInteger(id) || id <= 0) {
reject(new ValidationError("Ідентифікатор має бути додатним цілим числом"));
return;
}
if (id === 404) {
reject(new NotFoundError(`Користувача з id=${id} не знайдено`));
return;
}
resolve({
id,
name: "Олена",
role: "developer"
});
}, 100);
});
}
async function getUserProfile(id) {
try {
const user = await loadUser(id);
return {
...user,
label: `${user.name} — ${user.role}`
};
} catch (error) {
if (error instanceof ValidationError || error instanceof NotFoundError) {
// Очікувані помилки передаємо без зміни типу
throw error;
}
// Несподівану технічну помилку обгортаємо додатковим контекстом
throw new UserServiceError("Не вдалося отримати профіль користувача", error);
} finally {
// Тут можна звільнити ресурс або записати метрику
console.log(`Завершено обробку id=${id}`);
}
}
async function main() {
const ids = [1, 0, 404];
for (const id of ids) {
try {
const profile = await getUserProfile(id);
console.log("Профіль:", profile);
} catch (error) {
if (error instanceof ValidationError) {
console.error("Перевірте вхідні дані:", error.message);
} else if (error instanceof NotFoundError) {
console.error("Ресурс не знайдено:", error.message);
} else if (error instanceof UserServiceError) {
console.error("Помилка сервісу:", error.message);
console.error("Початкова причина:", error.cause.message);
} else {
console.error("Невідома помилка:", error);
}
}
}
}
main().catch((error) => {
// Захисний обробник для помилок, які не були перехоплені раніше
console.error("Критична помилка програми:", error);
});У цьому прикладі:
loadUser відхиляє Promise з конкретним типом помилки.
getUserProfile перехоплює помилку, але передає очікувані помилки далі.
finally виконується для кожного запиту.
main класифікує помилки та обирає реакцію.
Останній .catch() захищає від помилок, які не були оброблені в main.
Іноді корисно додати контекст, не втративши початкову помилку:
async function getData() {
try {
return await readFromStorage();
} catch (error) {
throw new Error("Помилка під час завантаження даних", {
cause: error
});
}
}У сучасних версіях Node.js початкову помилку можна отримати через error.cause:
try {
await getData();
} catch (error) {
console.error(error.message);
if (error.cause) {
console.error("Початкова помилка:", error.cause.message);
}
}Обгортання доречне, коли на поточному рівні з’являється важливий контекст: назва операції, ідентифікатор ресурсу або зовнішнього сервісу.
Не слід без потреби обгортати всі помилки в один загальний тип, якщо від цього втрачається можливість класифікувати їх.
awaitasync function save() {
try {
saveToDatabase();
} catch (error) {
console.error(error);
}
}Якщо saveToDatabase() повертає Promise, цей try...catch не дочекається його завершення.
Правильно:
async function save() {
try {
await saveToDatabase();
} catch (error) {
console.error(error);
}
}catchtry {
await operation();
} catch (error) {
// Нічого не робимо
}Такий код приховує проблему. Якщо помилка справді не потребує реакції на цьому рівні, її краще передати далі:
try {
await operation();
} catch (error) {
throw error;
}Або не перехоплювати її на цьому рівні взагалі.
try {
await operation();
} catch (error) {
throw new Error("Операція не вдалася");
}Початкова причина втрачена. Для діагностики краще зберегти її:
try {
await operation();
} catch (error) {
throw new Error("Операція не вдалася", {
cause: error
});
}ErrorНе рекомендується:
throw "Помилка";Краще:
throw new Error("Помилка");Об’єкт Error містить стек викликів і стандартні властивості, необхідні для діагностики.
console.errorЛогування не означає обробку бізнес-ситуації:
try {
await createOrder();
} catch (error) {
console.error(error);
}Потрібно вирішити, що має відбутися далі:
повернути помилку користувачу;
повторити операцію;
використати запасний сценарій;
передати помилку на вищий рівень;
завершити поточну операцію.
Під час проєктування async-коду дотримуйтеся таких правил:
Асинхронні операції очікуйте через await або повертайте їхній Promise.
Перехоплюйте помилки на рівні, де можна змістовно на них відреагувати.
Не приховуйте помилки порожнім catch.
Використовуйте власні класи для очікуваних категорій помилок.
Невідомі помилки передавайте далі.
Для завершальних дій використовуйте finally.
Під час обгортання помилки зберігайте її початкову причину.
Завжди викидайте об’єкти Error, а не рядки чи довільні значення.
async-функція завжди повертає Promise.
throw усередині async-функції створює відхилений Promise.
.catch() обробляє відхилені Promise та помилки з попередніх обробників.
try...catch разом з await дає змогу перехоплювати асинхронні помилки.
Помилки можна передавати вище через повторний throw.
finally виконується незалежно від успіху чи помилки.
Власні класи помилок допомагають розділяти валідаційні, прикладні та технічні проблеми.
Обробляти помилку потрібно там, де відома правильна реакція на неї.