Dlaczego nie mogę wrzucić do programu obsługi Promise.catch?

130

Dlaczego nie mogę po prostu wrzucić Errorwywołania zwrotnego wewnątrz catch i pozwolić procesowi obsłużyć błąd tak, jakby znajdował się w jakimkolwiek innym zakresie?

Jeśli nie zrobię, console.log(err)nic nie zostanie wydrukowane i nic nie wiem o tym, co się stało. Proces właśnie się kończy ...

Przykład:

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

Jeśli wywołania zwrotne są wykonywane w głównym wątku, dlaczego zostają Errorpołknięte przez czarną dziurę?

demian85
źródło
11
Nie zostaje połknięty przez czarną dziurę. Odrzuca obietnicę, która .catch(…)powraca.
Bergi
zamiast .catch((e) => { throw new Error() }), napisz .catch((e) => { return Promise.reject(new Error()) })lub po prostu.catch((e) => Promise.reject(new Error()))
chharvey
1
@chharvey wszystkie fragmenty kodu w Twoim komentarzu zachowują się dokładnie tak samo, z wyjątkiem tego, że początkowy jest oczywiście najbardziej jasny.
Сергей Гринько

Odpowiedzi:

161

Jak wyjaśnili inni, „czarna dziura” polega na tym, że wrzucenie do środka .catchkontynuuje łańcuch z odrzuconą obietnicą i nie ma już żadnych zaczepów, co prowadzi do niezakończonego łańcucha, który połyka błędy (źle!)

Dodaj jeszcze jeden haczyk, aby zobaczyć, co się dzieje:

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!
});

Zaczep w środku łańcucha jest przydatny, gdy chcesz, aby łańcuch działał pomimo nieudanego kroku, ale ponowne rzucenie jest przydatne, aby kontynuować niepowodzenie po wykonaniu takich czynności, jak rejestrowanie informacji lub kroki czyszczenia, być może nawet zmieniając który błąd Jest rzucony.

Sztuczka

Aby błąd pojawił się jako błąd w konsoli internetowej, zgodnie z pierwotnym zamysłem, używam tej sztuczki:

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

Nawet numery wierszy przetrwały, więc łącze w konsoli internetowej prowadzi mnie bezpośrednio do pliku i wiersza, w którym wystąpił (oryginalny) błąd.

Dlaczego to działa

Każdy wyjątek w funkcji nazywanej programem obsługi wypełnienia obietnicy lub odrzucenia jest automatycznie konwertowany na odrzucenie obietnicy, którą masz zwrócić. Dba o to kod obietnicy, który wywołuje twoją funkcję.

Z drugiej strony funkcja wywoływana przez setTimeout zawsze działa ze stabilnego stanu JavaScript, tj. Działa w nowym cyklu w pętli zdarzeń JavaScript. Wyjątki nie są przechwytywane przez nic i trafiają do konsoli internetowej. Ponieważ errzawiera wszystkie informacje o błędzie, w tym oryginalny stos, numer pliku i linii, nadal jest zgłaszany poprawnie.

wysięgnik
źródło
3
Jib, to interesująca sztuczka, czy możesz mi pomóc zrozumieć, dlaczego to działa?
Brian Keith
8
Co do tej sztuczki: rzucasz, ponieważ chcesz zalogować, więc dlaczego nie logować się bezpośrednio? Ta sztuczka w „przypadkowym” momencie spowoduje błąd, którego nie da się złapać ... Ale cała idea wyjątków (i sposobu, w jaki obietnice radzą sobie z nimi) polega na tym, aby osoba dzwoniąca była odpowiedzialna za wychwycenie błędu i uporanie się z nim. Ten kod skutecznie uniemożliwia wywołującemu obsługę błędów. Dlaczego po prostu nie stworzyć funkcji, która zajmie się tym za Ciebie? function logErrors(e){console.error(e)}następnie użyj go jak do1().then(do2).catch(logErrors). Przy okazji odpowiedź sama w sobie jest świetna, +1
Stijn de Witt
3
@jib Piszę lambdę AWS, która zawiera wiele obietnic powiązanych mniej więcej tak, jak w tym przypadku. Aby wykorzystać alarmy AWS i powiadomienia w przypadku błędów, muszę sprawić, aby awaria lambda wyrzucała błąd (chyba). Czy sztuczka jest jedynym sposobem, aby to osiągnąć?
masciugo
2
@StijndeWitt W moim przypadku próbowałem wysłać szczegóły błędu do mojego serwera w module window.onerrorobsługi zdarzeń. Tylko wykonując setTimeoutsztuczkę, można to zrobić. W przeciwnym razie window.onerrornigdy nie usłyszysz o błędach, które wystąpiły w Promise.
hudidit
1
@hudidit Mimo to, czy to jest console.loglub postErrorToServer, możesz po prostu zrobić to, co trzeba. Nie ma powodu, dla którego kod window.onerrornie może być rozłożony na oddzielną funkcję i wywołany z dwóch miejsc. Prawdopodobnie jest jeszcze krótsza niż setTimeoutlinia.
Stijn de Witt
46

Ważne rzeczy do zrozumienia

  1. Obie funkcje theni catchzwracają nowe obiekty obietnicy.

  2. Rzucenie lub jawne odrzucenie przesunie aktualną obietnicę do stanu odrzucenia.

  3. Ponieważ theni catchzwracają nowe obiecane obiekty, można je łączyć.

  4. Jeśli wrzucisz lub odrzucisz wewnątrz programu obsługi obietnic ( thenlub catch), zostanie to rozpatrzone w następnym programie obsługi odrzucenia na ścieżce łączenia.

  5. Jak wspomniano w jfriend00, programy obsługi theni catchnie są wykonywane synchronicznie. Kiedy przewodnik rzuca, natychmiast się kończy. Zatem stos zostanie rozwinięty, a wyjątek zostanie utracony. Dlatego rzucenie wyjątku odrzuca obecną obietnicę.


W twoim przypadku odrzucasz do środka do1, rzucając jakiś Errorprzedmiot. Teraz aktualna obietnica będzie w stanie odrzucenia, a kontrola zostanie przekazana kolejnemu handlerowi, co jest thenw naszym przypadku.

Ponieważ program thenobsługi nie ma modułu obsługi odrzucania, do2nie zostanie on w ogóle wykonany. Możesz to potwierdzić, używając console.logwewnątrz. Ponieważ bieżąca obietnica nie ma procedury obsługi odrzucenia, zostanie również odrzucona z wartością odrzucenia z poprzedniej obietnicy, a kontrola zostanie przekazana kolejnej obsłudze, którą jest catch.

Podobnie jak w catchprzypadku obsługi odrzucania, kiedy to zrobisz console.log(err.stack);, możesz zobaczyć ślad stosu błędów. Teraz wyrzucasz Errorz niego obiekt, więc obietnica zwrócona przez catchbędzie również w stanie odrzuconym.

Ponieważ nie dołączyłeś żadnej procedury obsługi odrzucania do programu catch, nie możesz obserwować odrzucenia.


Możesz podzielić łańcuch i lepiej to zrozumieć, w ten sposób

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

Wynik, który otrzymasz, będzie podobny do

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

Wewnątrz catchprocedury obsługi 1 otrzymujesz wartość promiseobiektu jako odrzuconą.

W ten sam sposób obietnica zwrócona przez catchhandlera 1 jest również odrzucana z tym samym błędem, z jakim promisezostała odrzucona i obserwujemy to w drugim catchhandlerze.

na czworo
źródło
4
Warto również dodać, że .then()procedury obsługi są asynchroniczne (stos jest rozwijany przed wykonaniem), więc wyjątki w nich muszą zostać zamienione na odrzucenia, w przeciwnym razie nie byłoby programów obsługi wyjątków, które mogłyby je złapać.
jfriend00
7

Wypróbowałem setTimeout()metodę opisaną powyżej ...

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

Irytujące okazało się, że jest to całkowicie nie do przetestowania. Ponieważ zgłasza asynchroniczny błąd, nie można go opakować w try/catchinstrukcję, ponieważ funkcja catchwill przestała nasłuchiwać do momentu zgłoszenia błędu .

Wróciłem do słuchania, które działało doskonale, a ponieważ tak powinno być używane JavaScript, było wysoce testowalne.

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

    /* Throw so the promise is still rejected for testing */
    throw err;
});
RiggerTheGeek
źródło
3
Jest ma makiety timera, które powinny poradzić sobie w tej sytuacji.
jordanbtucker
3

Zgodnie ze specyfikacją (patrz 3.III.d) :

re. W przypadku wywołania zgłasza wyjątek e,
  a. W przypadku wywołania funkcji determinacja i odrzucanie obietnicy, zignoruj ​​to.
  b. W przeciwnym razie odrzuć obietnicę, podając e jako powód.

Oznacza to, że jeśli wyrzucisz wyjątek w thenfunkcji, zostanie on przechwycony, a Twoja obietnica zostanie odrzucona.catchnie ma tu sensu, to tylko skrót do.then(null, function() {})

Chyba chcesz rejestrować nieobsłużone odrzucenia w swoim kodzie. Większość bibliotek obiecuje unhandledRejectionza to. Oto istotna treść dyskusji na ten temat.

Just-Boris
źródło
Warto wspomnieć, że unhandledRejectionhaczyk jest przeznaczony dla JavaScript po stronie serwera, po stronie klienta różne przeglądarki mają różne rozwiązania. Jeszcze tego nie znormalizowaliśmy, ale to powoli, ale pewnie.
Benjamin Gruenbaum
1

Wiem, że to trochę za późno, ale trafiłem na ten wątek i żadne z rozwiązań nie było dla mnie łatwe do wdrożenia, więc wymyśliłem własne:

Dodałem małą funkcję pomocniczą, która zwraca obietnicę, na przykład:

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

Następnie, jeśli mam określone miejsce w którymkolwiek z moich obietnic, w którym chcę rzucić błąd (i odrzucić obietnicę), po prostu wracam z powyższej funkcji z moim skonstruowanym błędem, na przykład:

}).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);
})

W ten sposób kontroluję wyrzucanie dodatkowych błędów z łańcucha obietnic. Jeśli chcesz również poradzić sobie z „normalnymi” błędami obietnic, możesz rozszerzyć swój chwyt, aby osobno traktować błędy „zgłaszane samodzielnie”.

Mam nadzieję, że to pomoże, to moja pierwsza odpowiedź!

nuudles
źródło
Promise.reject(error)zamiast new Promise(function (resolve, reject){ reject(error) })(co i tak wymagałoby oświadczenia zwrotnego)
Funkodebat
0

Tak, obiecuje połknąć błędy, a można je tylko złapać .catch , jak wyjaśniono bardziej szczegółowo w innych odpowiedziach. Jeśli jesteś w Node.js i chcesz odtworzyć normalne throwzachowanie, wydrukować ślad stosu do konsoli i zakończyć proces, możesz to zrobić

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Jesús Carrera
źródło
1
Nie, to nie wystarczy, ponieważ musiałbyś umieścić to na końcu każdego łańcucha obietnic, które masz. Raczej haczyk na unhandledRejectionwydarzeniu
Bergi
Tak, przy założeniu, że łączysz swoje obietnice w łańcuch, więc wyjście jest ostatnią funkcją i nie jest po nim przechwytywane. Wydarzenie, o którym wspominasz, wydaje mi się, że dotyczy to tylko Bluebird.
Jesús Carrera
Bluebird, Q, kiedy, rodzimy obiecuje… Prawdopodobnie stanie się standardem.
Bergi