(For those who are curious the full white paper for the framework can be found here.
)What are Await and Try…Catch?Async FunctionsBefore diving into await and try.
catch, a brief bit of background on async functions.
While it might take a bit of getting used to, async functions are indispensable for building web apps and are a necessary part of any developer’s arsenal.
At their core, async functions (no surprise) are used to run asynchronous code via the JavaScript Event Loop.
Their benefit (and perhaps what makes them potentially confusing at first) is that syntactically they look almost like synchronous functions, like so:async function foo() { return ‘hello world’;}function bar() { return ‘hello world’;}Other than the async keyword these function declarations look alike (also true for function expressions), but their usage and the code they contain can be radically different, as we will soon see.
An async function returns a Promise that is either resolved to the value the function would otherwise return, or rejected with an uncaught exception if something goes wrong during execution.
In the example above the async function foo would, if synchronous, otherwise return ’hello world’, and so it actually returns a Promise object resolved to that value.
Ultimately async functions provide some nice syntactical sugar over Promises, and provide a much nicer coding experience, especially when running multiple such functions back-to-back.
AwaitThis is where await comes in.
await must itself be used within the scope of an async function.
It behaves slightly differently depending on the expression that it is passed; either it is passed an object that is already a Promise, or if the value of the expression is not a Promise, it is converted to a Promise that is resolved to that value.
Either way we now have some Promise, and await pauses the execution of the surrounding async function until said Promise is either resolved or rejected.
The value of the entire await expression will be the value of the fulfilled Promise.
This may all seem like a bit to take in at first, so let’s consider some examples:let a;let b;let c;let d;async function foo() { // do stuff that might take a while return 3;}(async () => { a = await 1; b = await Promise.
resolve(2); c = await foo(); d = foo();})();What will the values of a, b, c, and d (eventually) be?.It’s probably easy to guess, but it’s important to understand precisely why the values are what they are.
We’ll start with a.
Clearly, 1 is of type Number, so what will JavaScript do?.First, the expression passed to await has value 1 and is not a Promise, so the expression is first converted to a Promise that is resolved to the value 1.
Then as stated above the value of the entire await expression will be that of the fulfilled Promise, i.
e.
1.
b similarly will end up with the value 2, the difference being that the expression passed to await is this time around already a fulfilled Promise, in particular with value 2.
What about c?.In this case we have to recall that the return value of an async function is a Promise resolved to the value that the function would otherwise (if synchronous) return.
foo would return 3, so the value returned by the async function call will (eventually) be a Promise resolved to 3, assuming no exception is thrown along the way.
Then the value of the await expression will be that of the returned Promise, so c will have the value 3.
Of course, depending on the stuff foo has to execute, it may take some time before the function returns, but this is the beauty of await!.Execution of the outer async function will be paused until the Promise implicit to foo’s invocation eventually resolves to 3.
Okay, d might be a bit of a gotcha; in fact, it’s not even being awaited.
This means that the outer async function may return while foo is still chugging along.
Eventually foo too should finish execution, but remember, d is not assigned the value of an await expression, but rather the return value of an async function.
The value of d will be a Promise object, resolved to 3, not 3 itself!How did you do?.If you got them all, great!.If not don’t worry, the key takeaway is that small details are important, and they make a big difference in the long run.
Some More SubtletiesSpeaking of small details, let’s take a look at some tricks and subtleties concerning await.
One technique that came up when our team built BAM!.(and happens all the time when developing apps) is delaying an async function for a given amount of time.
How would be use await to delay for, say 2000ms?.Remember, await pauses async function execution until a Promise is resolved, so logically we’ve solved the problem if we can pass to await a Promise that only resolves after two seconds…const delay = (time) => { return new Promise(resolve => setTimeout(resolve, time));}…like so.
Now you can delay… to your… heart’s content!(async () => { console.
log(‘hello…’); await delay(2000); console.
log(‘ world!’);})();To clarify, the Promise returned by delay only calls resolve after the interval of time passed to setTimeout, thus bringing about the pause in execution we are looking to achieve.
On the topic of execution, in what order will the following lines be run?(async () => { await console.
log(‘foo’); await console.
log(‘bar’); await console.
log(‘baz’); await console.
log(‘qux’);})();Unless you thought this was a trick question, you may have expected something like this to be logged:foobarbazqux…and you would be right!Each line within this async function call is only executed after the previous line; there is a subtle pitfall however, and it arguably stems from the fact that the syntactical sugar provided by await makes it almost look like synchronous code.
What, for example is the difference between the following lines versus what is above?console.
log(‘foo’);console.
log(‘bar’);console.
log(‘baz’);console.
log(‘qux’);Other than the obvious fact that the former is scoped to an async function, there is another subtle distinction.
If you’re thinking ahead, you might be wondering in which cases the code seen above blocks the JavaScript Event Loop.
By way of example, in what order will the following log?console.
log(1);(async () => { await delay(200); await console.
log(2); await console.
log(3); await console.
log(4);})();console.
log(5);Hint: it’s not 1, 2, 3, 4, 5.
Remember, await only pauses execution within the scope of its surrounding async function (and it can only be used within an async function in the first place).
It does not block the JavaScript Event Loop!.This means that while JavaScript waits to log 3 until after 2 is logged, outside the async function scope 5 will be logged as soon as possible.
Due to the 200ms delay, we’ll get something like 1, 5, 2, 3, 4.
The key here is that while await has the benefit of looking synchronous, it is non-blocking.
Try…CatchAll of the above examples involve a Promise or sequence of Promises that eventually all resolve, which does raise the question: what if one of them is rejected instead?.This is where try.
catch comes into play.
Without try.
catch let’s say you have something like this:const foo = async () => { throw ‘some error’;};(async () => { await console.
log(1); await console.
log(2); await foo(); await console.
log(3); await console.
log(4);})();foo will throw an error and the corresponding Promise will be rejected; since this rejection is unhandled 3 and 4 will not be logged, and an aptly named UnhandledPromiseRejectionWarning will be raised.
(In fact, at the time of this writing, unhandled promise rejections are deprecated in Node.
js; eventually such a rejection will terminate the entire Node.
js process.
)So how does one handle rejection?.This can be accomplished by using .
catch() on a Promise, but here we will focus on placing a try.
catch block within an async function:const foo = async () => { throw ‘some error’;};(async () => { try { await console.
log(1); await console.
log(2); await foo(); await console.
log(3); await console.
log(4); } catch (err) { console.
log(err); }})();This time, when a Promise is rejected within try, the remainder of the try block is skipped, and the thrown error is passed to the catch block, which is then executed.
In this example, we should see something like:12some errorOf course, if no rejection occurs, execution within try will proceed as normal, and the code within catch will never run.
In fact, here we are barely scratching the surface of the power try.
catch gives you.
For example there are also finally blocks that will run before any other code within the async function scope but after the try.
catch.
finally block.
finally will run whether or not an exception is thrown within try, and if an exception is thrown, it will even run regardless of whether the rejection is handled!. More details