Ланцюги промісів чудово підходять для обробки помилок. Якщо проміс завершує своє виконання з помилкою, то управління переходить в найближчий обробник помилок. На практиці, це дуже зручно.
Наприклад, в наведеному нижче прикладі для fetch
вказане неправильне посилання (такого сайту не існує), і .catch
перехоплює помилку:
fetch('https://no-such-server.blabla') // помилка
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (текст може відрізнятися)
Як можна побачити, .catch
не обов’язково повинен бути відразу після помилки, він може бути далі, після одного або навіть декількох .then
.
Або, можливо, з сервером все гаразд, але у відповіді ми отримуємо некоректний JSON. Найлегший шлях перехопити усі помилки – це додати .catch
в кінець ланцюжка:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
Якщо все гаразд, то такий .catch
взагалі не виконається. Але якщо будь-який з промісів буде відхилений (проблеми з мережею або некоректний json-рядок, або що завгодно інше), то помилка буде перехоплена.
Неявний try…catch
Навколо функції проміса та обробників знаходиться “невидимий try..catch
”. Якщо відбувається виключення, то воно перехоплюється, і проміс вважається відхиленим з цією помилкою.
Наприклад, цей код:
new Promise((resolve, reject) => {
throw new Error("Помилка!");
}).catch(alert); // Error: Помилка!
…Працює так само, як і цей:
new Promise((resolve, reject) => {
reject(new Error("Помилка!"));
}).catch(alert); // Error: Помилка!
“Невидимий try..catch
” навколо промісу автоматично перехоплює помилку і перетворює її на відхилений проміс.
Це працює не лише у функції проміса, але і в обробниках. Якщо ми створимо виключення (throw
) в обробнику (.then
), то проміс вважатиметься відхиленим, і управління перейде до найближчого обробника помилок.
Приклад:
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Помилка!"); // генеруємо помилку
}).catch(alert); // Error: Помилка!
Це відбувається для всіх помилок, не тільки для тих що викликані через оператор throw
. Наприклад, програмна помилка:
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
blabla(); // викликаємо неіснуючу функцію
}).catch(alert); // ReferenceError: blabla is not defined
Фінальний .catch
перехоплює як проміси, в яких викликаний reject
, так і випадкові помилки в обробниках.
Прокидання помилок
Як ми вже помітили .catch
поводиться як try..catch
. Ми можемо мати стільки обробників .then
, скільки ми хочемо, і потім використати один .catch
у кінці, щоб перехопити помилки з усіх обробників.
У звичайному try..catch
ми можемо проаналізувати помилку і повторно прокинути далі, якщо не можемо її обробити. Те ж саме можливе для промісів.
Якщо ми прокинемо (throw
) помилку усередині блоку .catch
, тоді управління перейде до наступного найближчого обробника помилок. А якщо ми обробимо помилку і завершимо роботу обробника нормально, то продовжить роботу найближчий успішний обробник .then
.
У прикладі нижче .catch
успішно перехоплює та обробляє помилку:
// the execution: catch -> then
new Promise((resolve, reject) => {
throw new Error("Помилка!");
}).catch(function(error) {
alert("Помилка оброблена, продовжуємо роботу");
}).then(() => alert("Управління переходить до наступного обробника then"));
Тут блок .catch
завершується нормально. Тому викликається наступний успішний обробник .then
.
У прикладі нижче ми бачимо іншу ситуацію з блоком .catch
. Обробник (*)
перехоплює помилку і не може обробити її (наприклад, він знає як обробити тільки URIError
), тому помилка прокидається далі:
// the execution: catch -> catch
new Promise((resolve, reject) => {
throw new Error("Помилка!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// обробляємо помилку
} else {
alert("Не можу обробити цю помилку");
throw error; // прокидуємо цю або іншу помилку в наступний catch
}
}).then(function() {
/* не виконається */
}).catch(error => { // (**)
alert(`Невідома помилка: ${error}`);
// нічого не повертаємо => виконання продовжується в нормальному режимі
});
Управління переходить від першого блоку .catch
(*)
до наступного (**)
, вниз по ланцюжку.
Необроблені помилки
Що станеться, якщо помилка не буде оброблена? Наприклад, ми просто забули додати .catch
в кінець ланцюжка, як тут:
new Promise(function() {
noSuchFunction(); // Помилка (немає такої функції)
})
.then(() => {
// обробники .then, один або більше
}); // без .catch в самому кінці!
У разі помилки виконання повинне перейти до найближчого обробника помилок. Але в прикладі вище немає ніякого обробника. Тому помилка як би “застряє”, її нікому обробити.
На практиці, як і при звичайних необроблених помилках в коді, це означає, що щось пішло сильно не так.
Що відбувається, коли звичайна помилка не перехоплена try..catch
? Скрипт помирає з повідомленням в консолі. Схоже відбувається і у разі необробленої помилки проміса.
JavaScript-рушій відстежує такі ситуації і генерує в цьому випадку глобальну помилку. Ви можете побачити її в консолі, якщо запустите приклад вище.
У браузері ми можемо впіймати такі помилки, використовуючи подію unhandledrejection
:
window.addEventListener('unhandledrejection', function(event) {
// об’єкт події має дві спеціальні властивості:
alert(event.promise); // [object Promise] - проміс, який згенерував помилку
alert(event.reason); // Error: Помилка! - об’єкт помилки, яка не була оброблена
});
new Promise(function() {
throw new Error("Помилка!");
}); // немає обробника помилок
Ця подія є частиною стандарту HTML.
Якщо відбувається помилка, і відсутній її обробник, то генерується подія unhandledrejection
, і відповідний об’єкт event
містить інформацію про помилку.
Зазвичай такі помилки невідворотні, тому краще всього – інформувати користувача про проблему і, можливо, відправити інформацію про помилку на сервер.
У небраузерних середовищах, таких як Node.js, є інші способи відстежування необроблених помилок.
Підсумки
.catch
перехоплює усі види помилок в промісах: будь то викликreject()
або помилка, кинута в обробнику за допомогоюthrow
..then
так само виловлює помилки, якщо надати другий аргумент (який є обробником помилок).- Необхідно розміщувати
.catch
там, де ми хочемо обробити помилки і знаємо, як це зробити. Обробник може проаналізувати помилку (можуть бути корисні призначені для користувача класи помилок) і прокинути її, якщо нічого не знає про неї (можливо, це програмна помилка). - Можна і зовсім не використовувати
.catch
, якщо немає нормального способу відновитися після помилки. - У будь-якому випадку нам слід використовувати обробник події
unhandledrejection
(для браузерів і аналог для іншого оточення), щоб відстежувати необроблені помилки і інформувати про них користувача (і, можливо, наш сервер), завдяки чому наш застосунок ніколи не буде “просто помирати”.