Ланцюги промісів чудово підходять для обробки помилок. Якщо проміс завершує своє виконання з помилкою, то управління переходить в найближчий обробник помилок. На практиці, це дуже зручно.
Наприклад, в наведеному нижче прикладі для 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
”. Якщо відбувається помилка, то її перехоплюють і опрацьовують як ніби був запущений reject
.
Наприклад, цей код:
new
Promise
(
(
resolve,
reject
)
=>
{
throw
new
Error
(
"Помилка!"
)
;
}
)
.
catch
(
alert)
;
// Error: Помилка!
…Працює так само, як і цей:
new
Promise
(
(
resolve,
reject
)
=>
{
reject
(
new
Error
(
"Помилка!"
)
)
;
}
)
.
catch
(
alert)
;
// Error: Помилка!
“Невидимий try..catch
” навколо промісу автоматично перехоплює помилку і перетворює її на відхилений проміс.
Це працює не лише в функції, яка створює проміс, але й в обробниках. Якщо ми викинемо виключення(exception) за допомогою 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
(для браузерів і аналог для іншого оточення), щоб відстежувати необроблені помилки і інформувати про них користувача (і, можливо, наш сервер), завдяки чому наш застосунок ніколи не буде “просто помирати”.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)