In the following Javascript code, why is the exception caught in example 1 and 2, but not in example 3?
const f1 = async () => {
console.log("f1()");
}
const f2 = async () => {
throw new Error("error from f2");
}
const errorHandler = (error) => {
console.error("caught in errorHandler: " error);
}
// Example 1 (caught):
f1().then(() => { throw new Error("error from anonymous") }).catch(errorHandler);
// Example 2 (caught):
f1().then(async () => { await f2(); }).catch(errorHandler);
// Example 3 (not caught):
f1().then(() => { f2(); }).catch(errorHandler);
In particular, examples 1 and 3 appear to be completely identical to me, but why is one caught and not the other?
CodePudding user response:
Because in Example 3 the result of the f2()
function is ignored. It is neither awaited not returned as part of the Promise chain.
Either await it:
f1().then(async () => { await f2(); }).catch(errorHandler);
Which will implicitly return a Promise
.
Or explicitly return its Promise
:
f1().then(() => f2()).catch(errorHandler);
or simply:
f1().then(f2).catch(errorHandler);
In particular, examples 1 and 3 appear to be completely identical to me
The key difference here is that Example 1 isn't doing anything asynchronous in the .then()
callback, whereas Example 3 is. Both throw an exception, but Example 3 throws that exception from within an asynchronous operation. And since that operation isn't being awaited, there's nothing to handle that exception.
CodePudding user response:
Evaluating the expression f2()
, f2
being an async
procedure, always returns a Promise
and promises are always settled (resolved or rejected) after current script finishes executing. With or without await
, f2()
just creates a task on the task queue, but with await
you're actually waiting for the promise it returns, to settle. Without await
, you aren't.
Consider the effect of adding a call to console.debug
after f2()
:
f1().then(() => { f2(); console.debug("Hello."); }).catch(errorHandler);
With the above, "Hello." will be printed on the console immediately after the preceding call to f2
returns with a promise, regardless whether the promise resolves or rejects. Like I said, procedures marked async
always return a promise, even for degenerate cases like e.g. the following:
const f3 = async () => 1; /// Returns a `Promise`, not `1`
console.assert(f3() instanceof Promise); /// Assertion valid because evaluating `f3()` yields a promise
console.assert((await f3()) instanceof Number); /// Assertion valid because evaluating `await f3()` yields `1`
Try console.debug(f3())
-- Promise
value will be printed. That's probably the piece of the puzzle you're missing -- it is the use of await
that causes well, waiting on the promise and throwing the value that the promise rejected with (if it did).
Now, if you look at your registering of errorHandler
with the catch
method in the third example, you're trying "catching" the error on the "wrong" promise. The promise returned by then
method being called on the promise returned by f1()
, is not the same promise as one returned by f2()
, nor are the two related in any way. These are different promises and again, because of the first factor, the promise returned by then
in your third example, doesn't reject -- it simply creates another promise with the f2()
expression there, which rejects "later" while the former is resolved with undefined
(because () => { f2(); }
does not return anything), not causing any procedures registered with the catch
method, to be called.
By the way, rejection of promises that aren't duly waited on -- no await
-- can be done with registering an event handler on the global object (window
in a Web browser, normally), for events of type unhandledrejection
. But that's rightfully a last-resort handling which typically is done for logging/telemetry purposes or as a catch-all to trap entire classes of error conditions.