Помилки в async-функціях
Помилки всередині async-функцій автоматично перетворюються на відхилений проміс (Promise.reject). Тому для їх обробки використовують або конструкцію try...catch усередині функції, або метод .catch() на промісі, який ця функція повертає.
Теорія
TL;DR
async-функція завжди повертає проміс, аthrowусередині неї стаєreject.awaitна відхиленому промісі кидає виняток, тож його ловить звичайнийtry...catch..catch()на виклику функції це друга рівноправна опція, зручна на межі застосунку.- Один
tryможе покривати кількаawait, або коженawaitдістає власнийtryдля точкової реакції. returnіз блокуcatch«лікує» ситуацію: проміс функції завершиться успішно зі значенням за замовчуванням.- У
Promise.allпадіння будь-якого проміса валить весь виклик, тож для часткових результатів берутьPromise.allSettled.
Швидкий приклад
async function getData() {
try {
const res = await fetch("https://no-such-domain.example/data");
const data = await res.json();
console.log(data);
} catch (error) {
console.error("Сталася помилка:", error.message);
}
}
getData();
// Вивід: Сталася помилка: fetch failedТут try...catch працює так само, як у синхронному коді, але ловить помилки з усіх await усередині блоку.
Чому це працює
await «зупиняє» виконання функції, поки проміс не завершиться. Якщо проміс відхилено, await генерує виняток, і управління передається в catch. Код після невдалого await не виконується взагалі.
async function example() {
const result = await Promise.reject("Помилка!");
console.log(result); // не виконається
}
example().catch(console.error);
// Вивід: Помилка!Це і є вся магія async/await: проміс, що відхилився, перетворюється на звичайний виняток, а звичайний виняток перетворюється назад на відхилений проміс на межі функції.
Спільна зона try і локальний контроль
Кілька await можна обгорнути в один try, якщо всі помилки обробляються однаково:
async function loadUserData() {
try {
const user = await fetch("/api/user");
const posts = await fetch("/api/posts");
const comments = await fetch("/api/comments");
} catch (e) {
console.error("Помилка під час завантаження даних:", e);
}
}Якщо будь-яка з операцій впаде, управління одразу перейде в catch, а решта запитів навіть не стартують.
Іноді краще обгортати окремі await, коли реакції мають бути різними:
async function example() {
try {
const user = await fetchUser();
} catch (e) {
console.error("Помилка під час завантаження користувача:", e);
}
try {
const posts = await fetchPosts();
} catch (e) {
console.error("Помилка під час завантаження постів:", e);
}
}Без try...catch проміс просто відхиляється
Якщо не обгортати await у try...catch, функція просто поверне відхилений проміс, який обробляють через .catch() на місці виклику:
async function fail() {
throw new Error("Щось пішло не так");
}
fail().catch(err => console.error("Спіймали:", err.message));
// Вивід: Спіймали: Щось пішло не такЦе повністю еквівалентно обробці .catch() у звичайних промісах.
Той самий .catch() можна навісити прямо на окремий await, коли треба просто «придушити» помилку і піти далі з безпечним значенням:
async function getUser() {
const res = await fetch("/api/user").catch(err => {
console.error("Помилка запиту:", err);
return null; // повертаємо безпечне значення
});
console.log("Результат:", res);
}
getUser();Якщо ж у блоці catch повернути значення, функція завершиться успішно, і воно потрапить у .then():
async function example() {
try {
await Promise.reject("Помилка!");
} catch (e) {
console.log("Спіймали:", e);
return "Значення за замовчуванням";
}
}
example().then(result => console.log("Результат:", result));
// Вивід:
// Спіймали: Помилка!
// Результат: Значення за замовчуваннямПомилки в паралельних await
Якщо ви виконуєте кілька операцій паралельно через Promise.all і хоча б один проміс відхилиться, весь Promise.all впаде з першою ж помилкою:
async function parallel() {
try {
const [a, b] = await Promise.all([
Promise.resolve("ok"),
Promise.reject("помилка"),
]);
} catch (e) {
console.log("Спіймали:", e);
}
}
parallel();
// Вивід: Спіймали: помилкаЯкщо потрібні результати всіх операцій незалежно від того, які з них упали, беріть Promise.allSettled: він завжди виконується успішно і повертає масив об'єктів зі status рівним "fulfilled" або "rejected".
Найкращі практики
| Ситуація | Що використати |
|---|---|
| Обробка помилок в одній функції | try...catch |
| Обробка помилок при виклику async-функції | .catch() |
| Різні типи помилок з різними реакціями | Кілька окремих try...catch |
Кілька паралельних await | Promise.allSettled() замість Promise.all |
| Логування і проброс далі | catch (e) { log(e); throw e; } |
Типові помилки
- Забути
awaitперед викликом. Функція поверне проміс, а не значення, і помилка не потрапить уtry...catch, який уже завершився. return promiseзамістьreturn await promiseу блоціtry. Безawaitвідхилення станеться вже після виходу зtry, тому локальнийcatchйого не побачить.- Викликати async-функцію без обробки взагалі. Це дає
unhandledrejectionу браузері й аварійне завершення процесу в Node.js 15 і новіших. - Вважати, що
fetchкидає помилку на 404 чи 500.fetchвідхиляє проміс лише на мережевому збої, тому статус треба перевіряти вручну черезres.ok. - Ловити помилку і мовчки її ковтати. Порожній
catchперетворює збій на успіх зundefined; або логуйте й робітьthrow e, або повертайте явне значення за замовчуванням. - Послідовні
awaitтам, де потрібні паралельні. Три незалежні запити в трьохawaitвиконаються один за одним; запускайте їх черезPromise.allабоPromise.allSettled. try...catchнавколо колбекаsetTimeout. Виняток із колбека таймера летить у глобальний обробник, а не у вашcatch; асинхронну операцію треба обгорнути в проміс.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.