Skip to main content

Обробка помилок у async-функціях

Будь-яка async-функція завжди повертає проміс, тому помилка всередині неї переводить цей проміс у стан rejected. Звідси й два основні способи обробки: try...catch усередині самої функції та .catch() зовні, під час виклику.

Теорія

TL;DR

  • async-функція завжди повертає проміс; throw усередині неї, це Promise.reject().
  • try...catch усередині ловить і явний throw, і відхилені проміси, на яких стоїть await.
  • .catch() зовні ловить ту саму помилку, але вже на місці виклику функції.
  • Ці способи комбінують: усередині логують і кидають нову помилку далі, зовні показують її користувачу.
  • Патерн [err, data] дозволяє обійтися без try...catch на кожному кроці.
  • Усе, що не спіймали, ловить глобальний обробник unhandledrejection.

Швидкий приклад

javascript
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();

Вивід:

text
Помилка всередині async: Failed to fetch

try...catch усередині функції

Це найзвичніший і найчитабельніший спосіб: код виглядає так само, як синхронний.

try...catch ловить:

  • помилки, викинуті через throw;
  • відхилені проміси, на яких стоїть await.

Працює це тому, що await під капотом розгортає rejection у звичайне виключення:

javascript
const result = await Promise.reject('failure'); // викине виключення, керування перейде в блок catch

Тобто відхилений проміс усередині async-функції поводиться так само, як звичайний throw, і його можна ловити тим самим try...catch.

.catch() зовні та ручний throw

Оскільки async-функція повертає Promise, помилку можна обробити звичайним .catch():

javascript
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));

Вивід:

text
Помилка зовні: Failed to fetch

Відмінність від try...catch у тому, що тут помилка обробляється під час виклику, а не всередині функції. Так само працює й ручний throw:

javascript
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));

Вивід:

text
OK

Якщо умова спрацює, throw перетвориться на Promise.reject(), і його впіймає зовнішній .catch().

Перехоплення з повторним throw і кілька рівнів try...catch

Інколи помилку треба обробити частково, наприклад залогувати, і передати далі:

javascript
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));

Вивід:

text
Помилка запиту: Failed to fetch Фінальна помилка: Помилка під час обробки даних

Якщо різні етапи мають різні сценарії відновлення, використовують кілька try...catch:

javascript
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

Інколи зручніше скористатися допоміжною функцією, яка ніколи не відхиляється, а повертає пару:

javascript
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 забули, помилку ще можна перехопити глобально:

javascript
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.