Чому я не можу закинути всередину обробника Promise.catch?


127

Чому я не можу просто закинути Errorвсередину зворотного виклику виклику і дозволити процесу обробляти помилку так, як ніби вона була в будь-якій іншій області застосування?

Якщо я нічого не роблю, я console.log(err)нічого не роздруковую і нічого не знаю про те, що сталося. Процес просто закінчується ...

Приклад:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

Якщо зворотні виклики виконуються в основному потоці, чому їх Errorпроковтує чорна діра?


11
Чорна діра її не проковтує. Він відкидає обіцянку, яка .catch(…)повертається.
Бергі


замість .catch((e) => { throw new Error() }), пишіть .catch((e) => { return Promise.reject(new Error()) })або просто.catch((e) => Promise.reject(new Error()))
chharvey

1
@chharvey всі фрагменти коду у вашому коментарі мають абсолютно однакову поведінку, за винятком того, що початковий очевидно є найбільш зрозумілим.
Сергій Гринько

Відповіді:


157

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

Додайте ще один улов, щоб побачити, що відбувається:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

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

Трюк

Щоб помилка відображалася як помилка у веб-консолі, як ви спочатку мали намір, я використовую цей трюк:

.catch(function(err) { setTimeout(function() { throw err; }); });

Навіть номери рядків виживають, тому посилання у веб-консолі переносить мене прямо на файл та рядок, де сталася (оригінальна) помилка.

Чому це працює

Будь-який виняток у функції, яка називається обробником виконання або відхилення обіцянки, автоматично перетворюється на відхилення обіцянки, яку ви повинні повернути. Про це піклується код обіцянки, який викликає вашу функцію.

З іншого боку, функція, яку викликає setTimeout, завжди працює з стабільного стану JavaScript, тобто працює в новому циклі в циклі подій JavaScript. Винятки там нічим не застають, і внесіть його до веб-консолі. Оскільки errзберігається вся інформація про помилку, включаючи оригінальний стек, номер файлу та рядок, вона все ще надходить правильно.


3
Джиб, це цікава хитрість, чи можете ви допомогти мені зрозуміти, чому це працює?
Брайан Кіт

8
Про цю хитрість: ви кидаєтесь, тому що хочете ввійти, то чому б не просто увійти безпосередньо? Цей трюк у "випадковий" момент призведе до нерозбірливої ​​помилки .... Але вся ідея винятків (і способів обіцяння з ними справлятися) полягає в тому, щоб покликати відповідальність за помилку і виправлення помилки. Цей код фактично унеможливлює поводження абонента з помилками. Чому б просто не зробити функцію, щоб обробити її за вас? function logErrors(e){console.error(e)}то використовуйте як do1().then(do2).catch(logErrors). Сама відповідь чудова btw, +1
Stijn de Witt

3
@jib Я пишу лямбду AWS, яка містить багато обіцянок, пов'язаних більш-менш, як у цьому випадку. Щоб використовувати сигнали тривоги та сповіщення AWS у разі помилок, мені потрібно зробити лямбда-аварію, кидаючи помилку (я думаю). Чи єдиний спосіб отримати це трюк?
masciugo

2
@StijndeWitt У моєму випадку я намагався надсилати дані про помилки на свій сервер у обробці window.onerrorподій. Тільки виконавши setTimeoutтрюк, це можна зробити. Інакше window.onerrorніколи не почуєш нічого про помилки, які сталися в Обіцянні.
hudidit

1
@hudidit Тим не менш, доки це console.logчи postErrorToServer, ти можеш просто робити те, що потрібно зробити. Немає ніякої причини, що в коді не window.onerrorможе бути розроблено в окрему функцію та викликатись з двох місць. Це, мабуть, навіть коротше, ніж setTimeoutлінія.
Штійн де Вітт

46

Тут важливі речі для розуміння

  1. І функції, thenі catchфункції повертають нові об'єкти обіцянки.

  2. Або кидання, або явне відхилення, перенесе поточну обіцянку до відхиленого стану.

  3. Оскільки thenі catchповертають нові об’єкти обіцянки, їх можна прикувати.

  4. Якщо ви кинете або відхилите всередину обробника обіцянки ( thenабо catch), він буде оброблятися в наступному обробника відхилення вниз по ланцюговому шляху.

  5. Як згадував jfriend00, обробники thenта catchобробники не виконуються синхронно. Коли обробник кине, це закінчиться негайно. Отже, стек буде розмотаний, а виняток буде втрачено. Ось чому кидання винятку відкидає поточну обіцянку.


У вашому випадку ви відкидаєте всередину do1, кидаючи Errorпредмет. Тепер поточна обіцянка буде у відхиленому стані, а контроль буде передано наступному обробнику, що є thenв нашому випадку.

Оскільки thenобробник не має обробника відхилення, він do2взагалі не буде виконаний. Ви можете підтвердити це, використовуючи console.logвсередині нього. Оскільки поточна обіцянка не має обробника відхилення, вона також буде відхилена зі значенням відхилення від попередньої обіцянки, і управління буде передано наступному оброблювачу, який є catch.

Як catchі обробник відхилень, коли ви робите console.log(err.stack);всередині нього, ви зможете побачити трасування стека помилок. Тепер ви кидаєте Errorз нього предмет, щоб обіцянка, повернута компанією catch, також буде у відхиленому стані.

Оскільки ви не приєднали жодного обробника відхилення до catch, ви не можете спостерігати за відхиленням.


Ви можете розколоти ланцюжок і зрозуміти це краще, як це

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

Вихід, який ви отримаєте, буде чимось на кшталт

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Всередині catchобробника 1 ви отримуєте значення promiseоб'єкта як відхиленого.

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


3
Можна також додати, що .then()обробники є асинхронними (стек розкручується до того, як вони будуть виконані), тому винятки всередині них повинні бути перетворені на відхилення, інакше не буде винятком обробників, щоб їх зловити.
jfriend00

7

Я спробував setTimeout()метод, детально описаний вище ...

.catch(function(err) { setTimeout(function() { throw err; }); });

Прикро, я вважав, що це абсолютно непереборно. Оскільки це викидання асинхронної помилки, ви не можете зафіксувати її всередині try/catchоператора, тому що catchволя перестала слухати, коли помилка часу викидається.

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

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});

3
Jest має макети таймерів, які повинні впоратися з цією ситуацією.
jordanbtucker

2

Відповідно до специфікації (див. 3.III.d) :

г. Якщо дзвінок, то викидає виняток e,
  a. Якщо було викликано резольпроміз або відхилитиPromise, ігноруйте його.
  б. В іншому випадку відкиньте обіцянку із причиною e.

Це означає, що якщо ви кинете виняток у thenфункції, він буде спійманий, і ваша обіцянка буде відхилена. catchне має сенсу тут, це просто ярлик.then(null, function() {})

Напевно, ви хочете внести необроблені відхилення у свій код. Більшість обіцянок бібліотеки викликають unhandledRejectionце. Тут доречна суть з обговоренням.


Варто згадати, що unhandledRejectionгачок призначений для серверного JavaScript, на клієнтській стороні різні браузери мають різні рішення. Ми ще не стандартизували це, але він потрапляє повільно, але впевнено.
Бенджамін Груенбаум

1

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

Я додав трохи допоміжної функції, яка повертає обіцянку, як-от так:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Тоді, якщо я маю певне місце в будь-якому ланцюжку моїх обіцянок, де я хочу видалити помилку (і відхилити обіцянку), я просто повертаюся з вищевказаної функції зі своєю побудованою помилкою, наприклад:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

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

Сподіваюся, це допомагає, це моя перша відповідь stackoverflow!


Promise.reject(error)замість new Promise(function (resolve, reject){ reject(error) })(для чого потрібен був би зворотний вислів)
Funkodebat

0

Так, обіцяє помилок проковтування, і ви можете їх лише наздогнати .catch, як детальніше пояснено в інших відповідях. Якщо ви перебуваєте в Node.js і хочете відтворити нормальну throwповедінку, друкуючи стек стека для консолі та виходу з процесу, ви можете це зробити

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});

1
Ні, цього недостатньо, як вам потрібно було б поставити це в кінці кожного ланцюжка обіцянок, який ви маєте. Швидше гак на unhandledRejectionподію
Бергі

Так, це припускаючи, що ви обіцяєте обіцянки, тому вихід є останньою функцією, і вона не буде перехоплена після. Подія, яку ви згадуєте, я вважаю, що це лише за умови використання Bluebird.
Jesús Carrera

Bluebird, Q, коли, рідні обіцяє… Це, швидше за все, стане стандартом.
Бергі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.