Why is it possible to try-catch an async-await call? - javascript

There is a common anti pattern in JavaScript:
function handleDataClb(err, data) {
if(!data) throw new Error('no data found');
// handle data...
}
function f() {
try {
fs.readFile('data', 'utf8', handleDataClb);
} catch(e) {
// handle error...
}
}
This try-catch in f will not catch errors in the handleDataClb as the callback is called in a later stage and context where the try-catch is not visible anymore.
Now in JavaScript async-await is implemented using generators, promises, and coroutines, as in:
// coroutine example
co(function* doTask() {
try {
const res1 = yield asyncTask1(); // returns promise
const res2 = yield asyncTask2(); // returns promise
return res1 + res2;
} catch(e) {
// handle error...
}
});
// async-await example
async function doTask() {
try {
const res1 = await asyncTask1(); // returns promise
const res2 = await asyncTask2(); // returns promise
return res1 + res2;
} catch(e) {
// handle error...
}
}
This way the try-catch works, which is often mentioned as one great advantage of async-await over callbacks.
Why and how does the catch work? How does the coroutine aka async manage to throw the error inside the try-catch when one of the asyncTask calls results in a promise rejection?
EDIT: as others have pointed out, the way how the JavaScript engine implements the await operator can be very different from the pure JavaScript implementation used by transpilers like Babel and shown above as coroutine example. Therefore to be more specific: how is this working using native JavaScript?

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:
It can evaluate like a plain expression, to the result value of that
It can evaluate like a throw statement, causing an exception
It can evaluate like a return statement, causing only finally statements to be evaluated before ending the function
On 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 awaited 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.

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.
Generator functions (Update)
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.

Related

Why isn't Promise.catch handler called for any exception in Promise.then handler?

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?
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.
Evaluating the expression f2(), f2 being an async procedure, always returns a Promise and promises are always settled (fulfilled 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 fulfills or rejects. The promise that catch is called on, fulfills with undefined -- because () => { f2(); console.debug("Hello."); } returns undefined / doesn't return anything nor does it throw -- f2() doesn't throw even if the promise it returns, was rejected.
Moving on, 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, if the promise is rejected, throwing the value it rejected with.
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 fulfilled 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 "listened on" 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, in quotes because you aren't handling the promise then, really -- it already rejected and whatever created it already had its chance to handle it but didn't which is why you end up with the "unhandled rejection" in the first place.

What is the difference between using and omitting keyword "await" in these two code blocks?

I'm following along an MDN article on async/await, and I understand the purpose of using the async keyword before functions, but I'm a little more confused about the await keyword. I've read up on on "await" as a result, and I get the general concept, but I'm still unsure when it comes to examples. For instance, here is a trivial piece of code (as shown in the MDN article) using async/await.
async function hello() {
return greeting = await Promise.resolve("Hello");
};
hello().then(value => console.log(value));
As you might expect, this logs "Hello" to the console. But even if we omit "await", it has the exact same output.
async function hello() {
return greeting = Promise.resolve("Hello"); // without await
};
hello().then(value => console.log(value));
Can someone help me understand exactly what the await keyword before Promise.resolve is doing? Why is the output the same even if it's omitted? Thanks.
In up-to-date JavaScript engines, there's no difference, but only because the return await somePromise is at the top level of the function code. If it were within a control block (for instance, try/catch), it would matter.
This is a recent-ish change. Until ES2020 the return await version would have introduced an extra async "tick" into the process. But that was optimized out in a "normative change" (rather than proposal) in ES2020 when the promise being awaited is a native promise. (For a non-native thenable, there's still a small difference.)
For the avoidance of doubt, there's an important difference between:
async function example() {
try {
return await somethingReturningAPromise();
} catch (e) {
// ...do something with the error/rejection...
}
}
and
async function example() {
try {
return somethingReturningAPromise();
} catch (e) {
// ...do something with the error/rejection...
}
}
In the former, a rejection of the promise from somethingReturningAPromise() goes to the catch block, because it's been awaited. In the latter, it doesn't, because you've just returned the promise, which resolves the promise from the async function to the promise somethingReturningAPromise() returns, without subjecting it to the catch.
But in your example, where it's not in any control structure, the await is largely a matter of style at this point.
Keep in mind that async/await is just a fancy API for working with promises.
An async function always and implicitly returns a promise (as soon as the first await or return statement is reached) that will be resolved when the function code itself finally reaches its end. The resolved promise value will be equal to the function's returned value.
async allows you to use await inside of the asynchronous scope. Await only makes sense when preceding a promise (could be another async function call, a promise created in place with new Promise(), etc.). Using await before non promise values will do no harm though.
await indicates, just that, wait. It tells the engine to 'stop' code execution on that scope and to resume it whenever the promise is fulfilled. await will then give you or 'return' the promise resolved value.
Just to give you some practical examples:
Using your first code:
async function hello() {
const greeting = await Promise.resolve("Hello"); // HERE I CAN USE await BECAUSE I'M INSIDE AN async SCOPE.
console.log('I will be printed after above promise is resolved.');
return greeting;
};
hello().then(value => console.log(value));
Using your second code, async could be completely removed:
function hello() {
return Promise.resolve("Hello");
};
hello().then(value => console.log(value));
Please let me know if there is any doubt.

JS Promises: Why does await have to be inside an async function?

Say I have the following code:
new Promise(res => res(1))
.then(val => console.log(val))
I can achieve the same thing with async/await like this:
let func = async () => {
let val = await new Promise(res => res(1))
console.log (val)
}
func()
I put the async/await code inside a function only because you have to be inside an async function in order to use await.
What I Want To Know: Why is this rule enforced? What would be the problem with just doing
let val = await new Promise(res => res(1))
console.log (val)
Is the reason that await causes the current scope to pause execution, and so forcing you to put the async/await code inside the special scope of an async function prevents JS from pausing the execution of all the rest of your code?
An async function is a different kind of function. It ALWAYS returns a promise. At the point of the first await that it hits, the function execution is suspended, the async function returns that promise and the caller gets the promise and keeps executing whatever code comes next.
In addition, the async function automatically wraps its function body in a try/catch so that any exceptions whether synchronous or unhandled rejected promises from an await are automatically caught by the async function and turned into a rejection of the promise they automatically return.
And, when you return a value from the async function, that return value becomes the resolved value of the promise that it returns.
What I Want To Know: Why is this rule enforced? What would be the problem with just doing...
An async function has a number of behaviors that a regular function doesn't and the JS interpreter wants to know ahead of time which type of function it is so it can execute it properly with the right type of behavior.
I suppose it might have been possible for the interpreter to have discovered when compiling the function body that it contains an await and automatically given the surrounding function an async behavior, but that's not very declarative and simply adding or removing one await could change how the function works entirely. Just my guess here, but the language designers decided it was much better to force an async function to be declared that way, rather than infer its behavior based on the contents of the function body.
The big picture here is to understand that an async function just works differently:
Always returns a promise
Automatically catches exceptions or rejected awaits and turns them into rejections
Suspends function execution upon await
Converts returned value into resolved value of the promise it returns
Chains an explicitly returned promise to the async-returned promise.
And, as such the language is clearer and more declarative if that separate behavior is spelled-out in the declaration with the async keyword rather than inferred from the function body.

In ES6, when throw is called, is the code immediately beneath the throw statement ever executed?

In node I am accustomed to handling errors with callbacks.
I'm trying to make the jump to async... await, which uses a method of error handling that I am not used to.
I am wondering, when throw is called, is the code immediately beneath the throw statement ever executed? In other words, do I need to do this...
async fetch() {
if (somethingBad) {
throw new Error('Cannot fetch')
} else {
let result = await this.read(this.get('id'))
return result
}
}
...or is it enough just to do this:
async fetch() {
if (somethingBad) throw new Error('Cannot fetch')
let result = await this.read(this.get('id'))
return result
}
In ES6, when throw is called, is the code immediately beneath the throw statement ever executed?
No, it is not. In an async function, throw means to immediately return a rejected promise. It's pretty much like doing this:
return Promise.reject(new Error('cannot fetch'));
It exits your function immediately and returns a rejected promise with the reason set to whatever value you used with throw.
One nice thing about async functions is that they automatically catch exceptions and turn them into rejected promises since the contract for an async function is that it always returns a promise.
FYI, in a non-async function (e.g. just a regular function), the rest of the function is not executed either. The function is immediately suspended. You've thrown an exception that will propagate up the call chain until if finds an exception handler or to the top level (where it will be an unhandled exception).

Is it legitimate to omit the 'await' in some cases?

I am using async/await in several places in my code.
For example, if I have this function:
async function func(x) {
...
return y;
}
Then I always call it as follows:
async function func2(x) {
let y = await func(x);
...
}
I have noticed that in some cases, I can omit the await and the program will still run correctly, so I cannot quite figure out when I must use await and when I can drop it.
I have concluded that it is "legitimate" to drop the await only directly within a return statement.
For example:
async function func2(x) {
...
return func(x); // instead of return await func(x);
}
Is this conclusion correct, or else, what am I missing here?
EDIT:
A small (but important) notion that has not been mentioned in any of the answers below, which I have just encountered and realized:
It is NOT "legitimate" to drop the await within a return statement, if the called function may throw an exception, and that statement is therefore executed inside a try block.
For example, removing the await in the code below is "dangerous":
async function func1() {
try {
return await func2();
}
catch (error) {
return something_else;
}
}
The reason is that the try block completes without an exception, and the Promise object returns "normally". In any function which calls the outer function, however, when this Promise object is "executed", the actual error will occur and an exception will be thrown. This exception will be handled successfully in the outer function only if await is used. Otherwise, that responsibility goes up, where an additional try/catch clause will be required.
If func is an async function then calling it with and without await has different effects.
async function func(x) {
return x;
}
let y = await func(1); // 1
let z = func(1) // Promise (resolves to 1)
It is always legitimate to omit the await keyword, but means you will have to handle the promises in the traditional style instead (defeating the point of async/await in the first place).
func(1).then(z => /* use z here */)
If your return statements use await then you can be sure that if it throws an error it can be caught inside your function, rather than by the code that calls it.
await just lets you to treat promises as values, when used inside an async function.
On the other hand, async works quite the opposite, it tags the function to return a promise, even if it happens to return a real, synchronous value (which sounds quite strange for an async function... but happens often when you have a function that either return a value or a promise based on conditions).
So:
I have concluded that it is "legitimate" to drop the await only directly within a return statement.
In the last return statement of an async function, you just are returning a Promise, either you are return actually a directly a promise, a real value, or a Promise-as-value with the await keyword.
So, is pretty redundant to use await in the return statement: you're using await to cast the promise to a value -in the context of that async execution-, but then the async tag of the function will treat that value as a promise.
So yes, is always safe to drop await in the last return statement.
PS: actually, await expects any thenable, i.e. an object that has a then property: it doesn't need a fully spec compliant Promise to work, afaik.
PS2: of course, you can always drop await keyword when invoking synchronous functions: it isn't needed at all.
An async function always returns a Promise.
So please keep in mind that these writing of an async function are all the same:
// tedious, sometimes necessary
async function foo() {
return new Promise((resolve) => resolve(1)))
}
// shorter
async function foo() {
return Promise.resolve(1)
}
// very concise but calling `foo` still returns a promise
async function foo() {
return 1 // yes this is still a promise
}
You call all of them via foo().then(console.log) to print 1. Or you could call them from another async function via await foo(), yet it is not always necessary to await the promise right away.
As pointed out by other answers, await resolves the promise to the actual return value statement on success (or will throw an exception on fail), whereas without await you get back only a pending promise instance that either might succeed or fail in the future.
Another use case of omitting (i.e.: being careful about its usage) await is that you might most likely want to parallelize tasks when writing async code. await can hinder you here.
Compare these two examples within the scope of an async function:
async function func() {
const foo = await tediousLongProcess("foo") // wait until promise is resolved
const bar = await tediousLongProcess("bar") // wait until promise is resolved
return Promise.resolve([foo, bar]) // Now the Promise of `func` is marked as a success. Keep in mind that `Promise.resolve` is not necessary, `return [foo, bar]` suffices. And also keep in mind that an async function *always* returns a Promise.
}
with:
async function func() {
promises = [tediousLongProcess("foo"), tediousLongProcess("bar")]
return Promise.all(promises) // returns a promise on success you have its values in order
}
The first will take significantly longer than the last one, as each await as the name implies will stop the execution until you resolve the first promise, then the next one.
In the second example, the Promise.all the promises will be pending at the same time and resolve whatever order, the result will then be ordered once all the promises have been resolved.
(The Bluebird promise library also provides a nice Bluebird.map function where you can define the concurrency as Promise.all might cripple your system.)
I only use await when want to work on the actual values. If I want just a promise, there is no need to await its values, and in some cases it may actually harm your code's performance.
I got a good answer above, here is just another explanation which has occurred to me.
Suppose I have this:
async function func(x) {
...
return y;
}
async function func2(x) {
...
return await func(x);
}
async function func3(x) {
let y = await func2(x);
...
}
The reason why I can safely remove the await in the return statement on func2, is that I already have an await when I call func2 in func3.
So essentially, in func3 above I have something like await await func(x).
Of course, there is no harm in that, so it's probably better to keep the await in order to ensure desired operation.

Categories