19 березня 2024 р.

Проміси: обробка помилок

Ланцюги промісів чудово підходять для обробки помилок. Якщо проміс завершує своє виконання з помилкою, то управління переходить в найближчий обробник помилок. На практиці, це дуже зручно.

Наприклад, в наведеному нижче прикладі для 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 (для браузерів і аналог для іншого оточення), щоб відстежувати необроблені помилки і інформувати про них користувача (і, можливо, наш сервер), завдяки чому наш застосунок ніколи не буде “просто помирати”.

Завдання

Що ви думаєте? Чи виконається .catch? Поясніть свою відповідь.

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

Відповідь: ні, не виконається:

new Promise(function(resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(alert);

Як було написано в розділі, тут присутній “прихований try..catch” навколо коду функції. Тому обробляються усі синхронні помилки.

У цьому прикладі помилка генерується не по ходу виконання коду, а пізніше. Тому проміс не може обробити її.

Навчальна карта