There is a common anti pattern in JavaScript:
function handleDataClb(err, data) {
if(!data) throw new Error(\'no data found\');
// handle data...
}
async
funtions
An async function returns a promise that is resolved by a value returned by the function body, or rejected by an error thrown in the body.
The await
operator returns the value of a fulfilled promise or throws an error, using the rejection reason, if the awaited promise is rejected.
Errors thrown by await
can be caught by try-catch blocks inside the async
function instead of allowing them to propagate up the execution stack and rejecting the promise returned by calling the async
function.
The await
operator also stores the execution context before returning to the event loop, in order to allow promise operations to proceed. When internally notified of settlement of the awaited promise, it restores the execution context before proceeding.
A try/catch
block set up in the async
function's execution context is not altered or rendered ineffective simply because the context has been saved and restored by await
.
As an aside
"async-await is implemented using generators, promises, and coroutines"
may be part of how Babel transpiles async
function and await
operator usage, but native implementations could be implemented more directly.
A generator function's execution context is stored in its associated generator object's internal [[Generator Context]] slot. (ECMA 2015 25.3.2)
Yield expressions remove the generator's execution context from the top of the execution context stack (25.3.3.5 of ES6/ECMAScript 2015)
Resuming a generator function restores the the function's execution context from the generator object's [[Generator Context]] slot.
Hence generator functions effectively restore the previous execution context when a yield
expression returns.
Throwing an error within a generator function for normal reasons (syntax error, a throw
statement, calling a function which throws) can be caught by a try-catch block as expected.
Throwing an error by means of Generator.prototype.throw()
throws an error within the generator function, originating from the yield expression
that last passed control from the generator function. This error can be trapped by try-catch
as for ordinary errors. (Refs MDN using throw(), ECMA 2015 25.3.3.4
Summary
Try-catch blocks around yield
statments used in await
transpilation code work for the same reason they do around await
operators within native async
functions - they are defined in the same execution context as the error is thrown for a rejected promise.
Why and how does the
catch
work? How does the coroutine aka async manage to throw the error inside the try-catch?
A yield
or await
expression can have 3 different outcomes:
throw
statement, causing an exceptionreturn
statement, causing only finally
statements to be evaluated before ending the functionOn a suspended generator, this can be achieved by calling either the .next(), .throw() or .return() methods. (Of course there's also a 4th possible outcome, to never get resumed).
…when one of the asyncTask calls results in a promise rejection?
The await
ed value will be Promise.resolve()
d to a promise, then the .then() method gets invoked on it with two callbacks: when the promise fulfills, the coroutine is resumed with a normal value (the promise result), and when the promise rejects, the coroutine is resumed with an abrupt completion (exception - the rejection reason).
You can look at the co library code or the transpiler output - it literally calls gen.throw from the promise rejection callback.