Обробка помилок у async-функціях
Будь-яка async-функція завжди повертає проміс, тому помилка всередині неї переводить цей проміс у стан rejected. Звідси й два основні способи обробки: try...catch усередині самої функції та .catch() зовні, під час виклику.
Теорія
TL;DR
async-функція завжди повертає проміс;throwусередині неї, цеPromise.reject().try...catchусередині ловить і явнийthrow, і відхилені проміси, на яких стоїтьawait..catch()зовні ловить ту саму помилку, але вже на місці виклику функції.- Ці способи комбінують: усередині логують і кидають нову помилку далі, зовні показують її користувачу.
- Патерн
[err, data]дозволяє обійтися безtry...catchна кожному кроці. - Усе, що не спіймали, ловить глобальний обробник
unhandledrejection.
Швидкий приклад
async function getData() {
try {
const res = await fetch('https://api.github.com/404'); // помилка мережі
const data = await res.json();
console.log('Отримано:', data);
} catch (error) {
console.error('Помилка всередині async:', error.message);
}
}
getData();Вивід:
Помилка всередині async: Failed to fetchtry...catch усередині функції
Це найзвичніший і найчитабельніший спосіб: код виглядає так само, як синхронний.
try...catch ловить:
- помилки, викинуті через
throw; - відхилені проміси, на яких стоїть
await.
Працює це тому, що await під капотом розгортає rejection у звичайне виключення:
const result = await Promise.reject('failure');
// викине виключення, керування перейде в блок catchТобто відхилений проміс усередині async-функції поводиться так само, як звичайний throw, і його можна ловити тим самим try...catch.
.catch() зовні та ручний throw
Оскільки async-функція повертає Promise, помилку можна обробити звичайним .catch():
async function getUser() {
const res = await fetch('https://api.example.com/missing');
return res.json();
}
getUser()
.then(console.log)
.catch(err => console.error('Помилка зовні:', err.message));Вивід:
Помилка зовні: Failed to fetchВідмінність від try...catch у тому, що тут помилка обробляється під час виклику, а не всередині функції. Так само працює й ручний throw:
async function example() {
const data = await Promise.resolve('OK');
if (!data) throw new Error('Немає даних!');
return data;
}
example()
.then(console.log)
.catch(err => console.error(err.message));Вивід:
OKЯкщо умова спрацює, throw перетвориться на Promise.reject(), і його впіймає зовнішній .catch().
Перехоплення з повторним throw і кілька рівнів try...catch
Інколи помилку треба обробити частково, наприклад залогувати, і передати далі:
async function processData() {
try {
const res = await fetch('https://api.example.com/missing');
return await res.json();
} catch (err) {
console.error('Помилка запиту:', err.message);
throw new Error('Помилка під час обробки даних');
}
}
processData().catch(err => console.error('Фінальна помилка:', err.message));Вивід:
Помилка запиту: Failed to fetch
Фінальна помилка: Помилка під час обробки данихЯкщо різні етапи мають різні сценарії відновлення, використовують кілька try...catch:
async function pipeline() {
try {
const res = await fetch('/api/data');
try {
const data = await res.json();
console.log('Дані:', data);
} catch (err) {
console.error('Помилка парсингу JSON:', err.message);
}
} catch (err) {
console.error('Помилка запиту:', err.message);
}
}Такий підхід корисний, коли збій мережі та невалідний JSON треба обробляти окремо.
Патерн [err, data] без try...catch
Інколи зручніше скористатися допоміжною функцією, яка ніколи не відхиляється, а повертає пару:
const to = (promise) => promise.then(data => [null, data]).catch(err => [err]);
async function run() {
const [err, data] = await to(fetch('/api/user').then(r => r.json()));
if (err) return console.error('Помилка:', err.message);
console.log(data);
}Цей патерн часто використовують у Node.js, щоб не писати try...catch на кожному кроці й тримати обробку помилок пласкою.
Глобальний перехоплювач unhandledrejection
Якщо .catch() або try...catch забули, помилку ще можна перехопити глобально:
window.addEventListener('unhandledrejection', (event) => {
console.error('Необроблений проміс:', event.reason);
});Це аналог try...catch для всіх промісів, у яких немає власного .catch(). У Node.js той самий сенс має process.on('unhandledRejection', handler). Такий обробник, це остання лінія оборони й місце для логування, а не заміна нормальної обробки.
| Спосіб | Де ловить помилку | Приклад |
|---|---|---|
try...catch | усередині async-функції | try { await ... } catch (e) { ... } |
.catch() | під час виклику функції | myAsync().catch(e => ...) |
| Глобальний перехоплювач | для всіх необроблених | window.addEventListener('unhandledrejection', ...) |
Патерн [err, data] | через обгортку | const [err, res] = await to(promise) |
Проста аналогія:
async-функція, це квест.await, це завдання, яке може провалитися;try...catch, це страховка, що дає змогу вирішити, як діяти далі;.catch()зовні, це гейм-майстер, який ловить фатальні помилки, коли гравець не впорався.
Типові помилки
- Забувати
awaitпередres.json()уreturn: у конструкціїtry { return res.json(); }помилка парсингу вилетить уже позаtry, і локальнийcatchїї не побачить. - Ловити помилку, нічого не робити й не кидати її далі: виклик отримає
undefinedі поводитиметься так, ніби все пройшло успішно. - Думати, що
fetchкидає виключення на HTTP 404 або 500. Він відхиляє проміс лише на мережевій помилці, статус треба перевіряти вручну черезres.ok. - Ставити
try...catchнавколо виклику асинхронної функції безawait: безawaitвиключення не потрапить у цей блок, а стане необробленим rejection. - Покладатися лише на глобальний
unhandledrejection: він спрацьовує запізно й не дає користувачу зрозумілого повідомлення. - Кидати далі рядок замість
Error: губиться стек викликів, іerr.messageбудеundefined.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.