Mastering Async Await
Mastering Async Await
Valeri Karpov
Table of Contents
0. How To Use This Book 1
1. Return Values 6
2. Error Handling 9
3. Retrying Failed Requests 12
4. Exercise 1: HTTP Request Loops 14
5. Exercise 2: Retrying Failed Requests 15
1. Promise Chaining 19
2. catch() and Other Helpers 23
3. Exercise 1: Promise Chains in Action 26
4. Exercise 2: Promise.race() 27
3. Async/Await Internals 28
1. await vs return 29
2. Concurrency 31
3. Async/Await vs Generators 33
4. Core Principles 35
5. Exercise 1: Implementing Custom Thenables 38
6. Exercise 2: Async forEach() 39
1. With Mocha 41
2. With Express 42
3. With MongoDB 44
4. With Redux 46
5. With React 48
6. Exercise 1: Does X Support Async/Await? 50
7. Exercise 2: WebSocket Integration 51
5. Moving On 52
Async/await promises to make asynchronous code as clean and easy to read as synchronous
code in most use cases. Tangled promise chains and complex user-land libraries like async can
be replaced with for loops, if statements, and try/catch blocks that even the most junior of
engineers can make sense of.
The following JavaScript from a 2012 blog post is a typical example of where code goes wrong
with callbacks. This code works, but it has a lot of error handling boilerplate and deeply nested if
statements that obfuscate the actual logic. Wrapping your mind around it takes a while, and
proper error handling means copy/pasting if (err != null) into every callback.
1
function getWikipediaHeaders() {
// i. check if headers.txt exists
fs.stat('./headers.txt', function(err, stats) {
if (err != null) { throw err; }
if (stats == undefined) {
// ii. fetch the HTTP headers
var options = { host: 'www.wikipedia.org', port: 80 };
http.get(options, function(err, res) {
if (err != null) { throw err; }
var headers = JSON.stringify(res.headers);
// iii. write the headers to headers.txt
fs.writeFile('./headers.txt', headers, function(err) {
if (err != null) { throw err; }
console.log('Great Success!');
});
});
} else { console.log('headers already collected'); }
});
}
Below is the same code using async/await, assuming that stat() , get() , and writeFile() are
properly promisi ed.
You might not think async/await is a big deal. You might even think async/await is a bad idea. I've
been in your shoes: when I rst learned about async/await in 2013, I thought it was unnecessary
at best. But when I started working with generator-based coroutines (the 2015 predecessor to
async/await), I was shocked at how quickly server crashes due to TypeError: Cannot read
property 'x' of undefined vanished. By the time async/await became part of the JavaScript
language spec in 2017, async/await was an indispensable part of my dev practice.
Just because async/await is now o cially part of JavaScript doesn't mean the world is all
sunshine and rainbows. Async/await is a new pattern that promises to make day-to-day
development work easier, but, like any pattern, you need to understand it or you'll do more harm
2
than good. If your async/await code is a patchwork of copy/pasted StackOver ow answers, you're
just trading callback hell for the newly minted async/await hell.
The purpose of this book is to take you from someone who is casually acquainted with promises
and async/await to someone who is comfortable building and debugging a complex app whose
core logic is built on async/await. This book is only 52 pages and is meant to be read in about 2
hours total. You may read it all in one sitting, but you would be better served reading one chapter
at a time, studying the exercises at the end, and getting a good night's sleep in between chapters
to really internalize the information.
This book is broken up into 4 chapters. Each chapter is 12 pages, including exercises at the end of
each chapter that highlight key lessons from the chapter. The exercises require more thought than
code and should be easy to answer within a few minutes.
The rst 3 chapters are focused on promise and async/await fundamentals, and strive to avoid
frameworks and outside dependencies. In particular, the code samples and exercises are meant
to run in Node.js 8.x and will not use any transpilers like Babel.
In the interest of providing realistic examples, the code samples will use the superagent module
for making HTTP requests. The 4th chapter will discuss integrating async/await with some
common npm modules.
If you nd any issues with the code samples or exercises, please report them at
github.com/vkarpov15/mastering-async-await-issues.
test();
You can use the await keyword anywhere in the body of an async function. This means you can
use await in if statements, for loops, and try/catch blocks. Below is another way to pause
an async function's execution for about 1 second.
Example 1.2
test();
There is one major restriction for using await : you can only use await within the body of a
function that's marked async . The following code throws a SyntaxError .
Example 1.3
function test() {
const p = new Promise(resolve => setTimeout(() => resolve(), 1000));
// SyntaxError: Unexpected identifier
await p;
}
test();
4
In particular, you can't use await in a closure embedded in an async function, unless the closure
is also an async function. The below code also throws a SyntaxError .
Example 1.4
As long as you don't create a new function, you can use await underneath any number of for
loops and if statements.
Example 1.5
Return Values 5
Return Values
You can use async/await for more than just pausing execution. The return value of await is the
value the promise is ful lled with. This means you can assign a variable to an asynchronously-
computed value in code that looks synchronous.
Example 1.6
An async function always returns a promise. When you return from an async function,
JavaScript resolves the promise to the value you returned. This means calling async functions
from other async functions is very natural. You can await on the async function call and get the
async function's "return value".
Example 1.7
This book will refer to the value you return from an async function as the resolved value. In
computeValue above, "Hello, World!" is the resolved value, computeValue() still returns a
6
promise. This distinction is subtle but important: the value you return from an async function
body is not the value that an async function call like computeValue() without await returns.
You can also return a promise from an async function. In that case, the promise the async
function returns will be ful lled or rejected whenever the resolved value promise is ful lled or
rejected. Below is another async function that ful lls to 'Hello, World!' after 1 second:
Example 1.8
If you return a promise from an async function, the resolved value will still not equal the return
value. The below example demonstrates that the resolvedValue promise that the function body
returns is not the same as the return value from computeValue() .
Example 1.9
Async/await beginners often mistakenly think they need to return a promise from an async
function. They likely read that an async function always returns a promise and think they're
responsible for returning a promise. An async function always returns a promise, but, like in
example 1.9, JavaScript creates the returned promise for you.
Example 1.10
Error Handling 7
Error Handling
One of the most important properties of async/await is that you can use try/catch to handle
asynchronous errors. Remember that a promise may be either ful lled or rejected. When a
promise p is ful lled, JavaScript evaluates await p to the promise's value. What about if p is
rejected?
Example 1.11
If p is rejected, await p throws an error that you can catch with a normal JavaScript try/catch .
Note that the await statement is what throws an error, not the promise instantiation.
This try/catch behavior is a powerful tool for consolidating error handling. The try/catch
block above can catch synchronous errors as well as asynchronous ones. Suppose you have code
that throws a TypeError: cannot read property 'x' of undefined error:
Example 1.12
In callback-based code, you had to watch out for synchronous errors like TypeError separately
from asynchronous errors. This lead to a lot of server crashes and red text in Chrome consoles,
because discipline doesn't scale.
Consider using a callback-based approach instead of async/await. Suppose you have a black-box
function test() that takes a single parameter, a callback . If you want to ensure you catch every
possible error, you need 2 try/catch calls: one around test() and one around callback() .
8
You also need to check whether test() called your callback with an error. In other words, every
single async operation needs 3 distinct error handling patterns!
Example 1.13
function testWrapper(callback) {
try {
// There might be a sync error in `test()`
test(function(error, res) {
// `test()` might also call the callback with an error
if (error) {
return callback(error);
}
// And you also need to be careful that accessing `res.x` doesn't
// throw **and** calling `callback()` doesn't throw.
try {
return callback(null, res.x);
} catch (error) {
return callback(error);
}
});
}
}
When there's this much boilerplate for error handling, even the most rigorous and disciplined
developers end up missing a spot. The result is uncaught errors, server downtime, and buggy user
interfaces. Below is an equivalent example with async/await. You can handle the 3 distinct error
cases from example 1.12 with a single pattern.
Example 1.14
Let's take a look at how the throw keyword works with async functions now that you've seen how
try/catch works. When you throw in an async function, JavaScript will reject the returned
promise. Remember that the value you return from an async function is called the resolved
9
value. Similarly, this book will refer to the value you throw in an async function as the rejected
value.
Example 1.15
Remember that the computeValue() function call itself does not throw an error in the test()
function. The await keyword is what throws an error that you can handle with try/catch . The
below code will print "No Error" unless you uncomment the await block.
Example 1.16
10
Just because you can try/catch around a promise doesn't necessarily mean you should. Since
async functions return promises, you can also use .catch() :
Example 1.17
Both try/catch and catch() have their place. In particular, catch() makes it easier to
centralize your error handling. A common async/await novice mistake is putting try/catch at the
top of every single function. If you want a common handleError() function to ensure you're
handing all errors, you're better off using catch() .
Example 1.18
// Do this instead
async function fn2() {
/* Bunch of logic here */
}
fn2().catch(handleError);
With callbacks or promise chains, retrying failed requests requires recursion, and recursion is less
readable than the synchronous alternative of writing a for loop. Below is a simpli ed
implementation of a getWithRetry() function using callbacks and the superagent HTTP client.
Example 1.19
Recursion is subtle and tricky to understand relative to a loop. Plus, the above code ignores the
possibility of sync errors, because the try/catch spaghetti highlighted in example 1.13 would
make this example unreadable. In short, this pattern is both brittle and cumbersome.
With async/await, you don't need recursion and you need one try/catch to handle sync and
async errors. The async/await implementation is built on for loops, try/catch , and other
constructs that should be familiar to even the most junior of engineers.
Example 1.20
12
More generally, async/await makes executing async operations in series trivial. For example, let's
say you had to load a list of blog posts from an HTTP API and then execute a separate HTTP
request to load the comments for each blog post. This example uses the excellent
JSONPlaceholder API that provides good test data.
Example 1.21
If this example seems trivial, that's good, because that's how programming should be. The
JavaScript community has created an incredible hodge-podge of tools for executing
asynchronous tasks in series, from async.waterfall() to Redux sagas to zones to co.
Async/await makes all of these libraries and more unnecessary. Do you even need Redux
middleware anymore?
This isn't the whole story with async/await. This chapter glossed over numerous important details,
including how promises integrate with async/await and what happens when two asynchronous
functions run simultaneously. Chapter 2 will focus on the internals of promises, including the
difference between "resolved" and "ful lled", and explain why promises are perfectly suited for
async/await.
Below are the API endpoints. The API endpoints are hosted on Google Cloud Functions at
https://us-central1-mastering-async-await.cloudfunctions.net .
{ "src":"./lib/posts/20160304_circle_ci.md",
"title":"Setting Up Circle CI With Node.js",
"date":"2016-03-04T00:00:00.000Z",
"tags":["NodeJS"],
"id":51 }
/post?id=${id} gets the markdown content of a blog post by its id property. The above
blog post has id = 0, so you can get its content from this endpoint: https://us-central1-
mastering-async-await.cloudfunctions.net/post?id=0 . Try opening this URL in your
browser, the output looks like this:
Loop through the blog posts and nd the id of the rst post whose content contains the string
"async/await hell".
Below is the starter code. You may copy this code and run it in Node.js using the node-fetch
npm module, or you may complete this exercise in your browser on CodePen at
http://bit.ly/async-await-exercise-1
For this exercise, you need to implement the getWithRetry() function below. This function
should fetch() the url , and if the request fails this function should retry the request up to
numRetries times. If you see "Correct answer: 76", congratulations, you completed this exercise.
Like exercise 1.1, you can complete this exercise locally by copying the below code and using the
node-fetch npm module. You can also complete this exercise in your browser on CodePen at the
following url: http://bit.ly/async-await-exercise-2.
In the ES6 spec, a promise is a class whose constructor takes an executor function. Instances of
the Promise class have a then() function. Promises in the ES6 spec have several other
properties, but for now you can ignore them. Below is a skeleton of a simpli ed Promise class.
Example 2.1
class Promise {
// `executor` takes 2 parameters, `resolve()` and `reject()`.
// The executor function is responsible for calling `resolve()`
// or `reject()` when the async operation succeeded or failed
constructor(executor) {}
pending: the initial state, means that the underlying operation is in progress
ful lled: the underlying operation succeeded and has an associated value
rejected: the underlying operation failed and has an associated error
A promise that is not pending is called settled. In other words, a settled promise is either ful lled
or rejected. Once a promise is settled, it cannot change state. For example, the below promise will
remain ful lled despite the reject() call. Once you've called resolve() or reject() once,
calling resolve() or reject() is a no-op. This detail is pivotal for async/await, because how
would await work if a promise changed state from 'FULFILLED' to 'REJECTED' after an async
function was done?
Example 2.2
16
Below is a diagram showing the promise state machine.
SETTLED
FULFILLED
PENDING
REJECTED
With this in mind, below is a rst draft of a promise constructor that implements the state
transitions. Note that the property names state , resolve , reject , and value used below are
non-standard. Actual ES6 promises do not expose these properties publicly, so don't try to use
p.value or p.resolve() with a native JavaScript promise.
Example 2.4
class Promise {
constructor(executor) {
this.state = 'PENDING';
this.chained = []; // Not used yet
this.value = undefined;
try {
// Reject if the executor throws a sync error
executor(v => this.resolve(v), err => this.reject(err));
} catch (err) { this.reject(err); }
}
// Define `resolve()` and `reject()` to change the promise state
resolve(value) {
if (this.state !== 'PENDING') return;
this.state = 'FULFILLED';
this.value = value;
}
reject(value) {
if (this.state !== 'PENDING') return;
this.state = 'REJECTED';
this.value = value;
}
}
17
The promise constructor manages the promise's state and calls the executor function. You also
need to implement the then() function that let clients de ne handlers that run when a promise is
settled. The then() function takes 2 function parameters, onFulfilled() and onRejected() . A
promise must call the onFulfilled() callback if the promise is ful lled, and onRejected() if
the promise is rejected.
For now, then() is simple, it push onFulfilled() and onRejected() onto an array chained .
Then, resolve() and reject() will call them when the promise is ful lled or rejected. If the
promise is already settled, the then() function will queue up onFulfilled() or onRejected()
to run on the next tick of the event loop using setImmediate() .
Example 2.5
class Promise {
// Constructor is the same as before, omitted for brevity
then(onFulfilled, onRejected) {
const { value, state } = this;
// If promise is already settled, enqueue the right handler
if (state === 'FULFILLED') return setImmediate(onFulfilled, value);
if (state === 'REJECTED') return setImmediate(onRejected, value);
// Otherwise, track `onFulfilled` and `onRejected` for later
this.chained.push({ onFulfilled, onRejected });
}
resolve(value) {
if (this.state !== 'PENDING') return;
this.state = 'FULFILLED';
this.value = value;
// Loop through the `chained` array and find all `onFulfilled()`
// functions. Remember that `.then(null, onRejected)` is valid.
this.chained.
filter(({ onFulfilled }) => typeof onFulfilled === 'function').
// The ES6 spec section 25.4 says `onFulfilled` and
// `onRejected` must be called on a separate event loop tick
forEach(({ onFulfilled }) => setImmediate(onFulfilled, value));
}
reject(value) {
if (this.state !== 'PENDING') return;
this.state = 'REJECTED';
this.value = value;
this.chained.
filter(({ onRejected }) => typeof onRejected === 'function').
forEach(({ onFulfilled }) => setImmediate(onFulfilled, value));
}
}
18
This Promise class, while simple, represents most of the work necessary to integrate with
async/await. The await keyword doesn't explicitly check if the value it operates on is
instanceof Promise , it only checks for the presence of a then() function. In general, any
object that has a then() function is called a thenable in JavaScript. Below is an example of using
the custom Promise class with async/await.
Example 2.6
Promise Chaining
One key feature that the promise implementation thus far does not support is promise chaining.
Promise chaining is a common pattern for keeping async code at, although it has become far
less useful now that generators and async/await have widespread support. Here's how the
getWikipediaHeaders() function from the introduction looks with promise chaining:
Example 2.7
function getWikipediaHeaders() {
return stat('./headers.txt').
then(res => {
if (res == null) {
// If you return a promise from `onFulfilled()`, the next
// `then()` call's `onFulfilled()` will get called when
// the returned promise is fulfilled...
return get({ host: 'www.wikipedia.org', port: 80 });
}
return res;
}).
then(res => {
// So whether the above `onFulfilled()` returns a primitive or a
// promise, this `onFulfilled()` gets the headers object
return writeFile('./headers.txt', JSON.stringify(res.headers));
}).
then(() => console.log('Great success!')).
catch(err => console.err(err.stack));
}
19
While async/await is a superior pattern, promise chaining is still useful, and still necessary to
complete a robust promise implementation. In order to implement promise chaining, you need to
make 3 changes to the promise implementation from example 2.5:
1. The then() function needs to return a promise. The promise returned from then() should be
resolved with the value returned from onFulfilled()
2. The resolve() function needs to check if value is a thenable, and, if so, transition to ful lled
or rejected only when value transitions to ful lled or rejected.
3. If resolve() is called with a thenable, the promise needs to stay 'PENDING', but future calls
to resolve() and reject() must be ignored.
The rst change, improving the then() function, is shown below. There are two other changes:
onFulfilled() and onRejected() now have default values, and are wrapped in a try/catch.
Example 2.8
then(_onFulfilled, _onRejected) {
// `onFulfilled` is a no-op by default...
if (typeof _onFulfilled !== 'function') _onFulfilled = (v => v);
// and `onRejected` just rethrows the error by default
if (typeof _onRejected !== 'function') {
_onRejected = err => { throw err; };
}
return new Promise((resolve, reject) => {
// Wrap `onFulfilled` and `onRejected` for two reasons:
// consistent async and `try/catch`
const onFulfilled = res => setImmediate(() => {
try {
resolve(_onFulfilled(res));
} catch (err) { reject(err); }
});
const onRejected = err => setImmediate(() => {
try {
// Note this is `resolve()`, **not** `reject()`. The `then()`
// promise will be fulfilled if `onRejected` doesn't rethrow
resolve(_onRejected(err));
} catch (err) { reject(err); }
});
20
Now then() returns a promise. However, there's still work to be done: if onFulfilled() returns
a promise, resolve() needs to be able to handle it. In order to support this, the resolve()
function will need to use then() in a two-step recursive dance. Below is the expanded
resolve() function that shows the 2nd necessary change.
Example 2.9
resolve(value) {
if (this.state !== 'PENDING') return;
if (value === this) {
return this.reject(TypeError(`Can't resolve promise with itself`));
}
// Is `value` a thenable? If so, fulfill/reject this promise when
// `value` fulfills or rejects. The Promises/A+ spec calls this
// process "assimilating" the other promise (resistance is futile).
const then = this._getThenProperty(value);
if (typeof then === 'function') {
try {
return then.call(value, v => this.resolve(v),
err => this.reject(err));
} catch (error) {
return reject(error);
}
}
21
Finally, the third change, ensuring that a promise doesn't change state once resolve() is called
with a thenable, requires changes to both resolve() and the promise constructor. The
motivation for this change is to ensure that p2 in the below example is ful lled, not rejected.
Example 2.10
One way to achieve this is to create a helper function that wraps this.resolve() and
this.reject() that ensures resolve() and reject() can only be called once.
Example 2.11
Once you have this _wrapResolveReject() helper, you need to use it in resolve() :
Example 2.12
resolve(value) {
// ...
if (typeof then === 'function') {
// If `then()` calls `resolve()` with a 'PENDING' promise and then
// throws, the `then()` promise will be fulfilled like example 2.10
const { resolve, reject } = this._wrapResolveReject();
try {
return then.call(value, resolve, reject);
} catch (error) { return reject(error); }
}
22
// ...
}
23
Also, you need to use _wrapResolveReject() in the constructor itself:
Example 2.13
With all these changes, the complete promise implementation, which you can nd at bit.ly/simple-
promise, now passes all 872 test cases in the Promises/A+ spec. The Promises/A+ spec is a
subset of the ES6 promise spec that focuses on then() and the promise constructor.
The catch() function may sound complex, but it is just a thin layer of syntactic sugar on top of
then() . The catch() is so sticky because the name catch() is a powerful metaphor for
explaining what this helper is used for. Below is the full implementation of catch() .
Example 2.14
catch(onRejected) {
return this.then(null, onRejected);
}
Why does this work? Recall from example 2.8 that then() has a default onRejected() argument
that rethrows the error. So when a promise is rejected, subsequent then() calls that only specify
an onFulfilled() handler are skipped.
Example 2.15
24
There are several other helpers in the ES6 promise spec. The Promise.resolve() and
Promise.reject() helpers are both commonly used for testing and examples, as well as to
convert a thenable into a fully edged promise.
Example 2.16
static resolve(v) {
return new Promise(resolve => resolve(v));
}
static reject(err) {
return new Promise((resolve, reject) => reject(err));
}
The Promise.all() function is another important helper, because it lets you execute multiple
promises in parallel and await on the result. The below code will run two instances of the run()
function in parallel, and pause execution until they're both done.
Example 2.18
console.log('Start running');
await Promise.all([run(), run()]);
console.log('Done');
// Start running
// run(): running
// run(): running
// run(): done
// run(): done
// Done
25
Promise.all() is the preferred mechanism for executing async functions in parallel. To execute
async functions in series, you would use a for loop and await on each function call.
Promise.all() is just a convenient wrapper around calling then() on an array of promises and
waiting for the result. Below is a simpli ed implementation of Promise.all() :
Example 2.19
static all(arr) {
let remaining = arr.length;
if (remaining === 0) return Promise.resolve([]);
// `result` stores the value that each promise is fulfilled with
let result = [];
return new Promise((resolve, reject) => {
// Loop through every promise in the array and call `then()`. If
// the promise fulfills, store the fulfilled value in `result`.
// If any promise rejects, the `all()` promise rejects immediately.
arr.forEach((p, i) => p.then(
res => {
result[i] = res;
--remaining || resolve(result);
},
err => reject(err)));
});
}
There is one more helper function de ned in the ES6 spec, Promise.race() , that will be an
exercise. Other than race() and some minor details like support for subclassing, the promise
implementation in this chapter is compliant with the ES6 spec. In the next chapter, you'll use your
understanding of promises to monkey-patch async/await and gure out what's happening under
the hood.
The key takeaways from this journey of building a promise library from scratch are:
A promise can be in one of 3 states: pending, ful lled, or rejected. It can also be locked in to
match the state of another promise if you call resolve(promise) .
Once a promise is settled, it stays settled with the same value forever
The then() function and the promise constructor are the basis for all other promise
functions. The catch() , all() , resolve() , and reject() helpers are all syntactic sugar on
top of then() and the constructor.
But before you start tinkering with the internals of async/await, here's 2 exercises to expand your
understanding of promises.
Using the same endpoints as Exercise 1.1, which are explained below, nd the blog post entitled
"Unhandled Promise Rejections in Node.js", load its content, and nd the number of times the
phrase "async/await" appears in the content .
Below are the API endpoints. The API endpoints are hosted on Google Cloud Functions at
https://us-central1-mastering-async-await.cloudfunctions.net
{ "src":"./lib/posts/20160304_circle_ci.md",
"title":"Setting Up Circle CI With Node.js",
"date":"2016-03-04T00:00:00.000Z",
"tags":["NodeJS"],
"id":51 }
/post?id=${id} gets the markdown content of a blog post by its id property. The above
blog post has id = 0, so you can get its content from this endpoint: https://us-central1-
mastering-async-await.cloudfunctions.net/post?id=0 . Try opening this URL in your
browser, the output looks like this:
Below is the starter code. You may copy this code and run it in Node.js using the node-fetch
npm module, or you may complete this exercise in your browser on CodePen at
http://bit.ly/async-await-exercise-21
function run() {
// Example of using `fetch()` API
return fetch(`${root}/posts`).
then(res => res.json()).
then(posts => console.log(posts[0]));
}
run().catch(error => console.error(error.stack));
Exercise 2: Promise.race() 27
Exercise 2: Promise.race()
The ES6 promise spec has one more helper method that this book hasn't covered yet:
Promise.race() . Like Promise.all() , Promise.race() takes in an array of promises, but
Promise.race() returns a promise that resolves or rejects to the same value that the rst
promise to settle resolves or rejects to. For example:
Implement a function race() , that, given an array of promises, returns a promise that resolves or
rejects as soon as one of the promises in the array settles, with the same value.
Below is the starter code. You may copy this code and complete this exercise in Node.js, or you
may complete it in your browser on CodePen at http://bit.ly/async-await-exercise-22 .
function race(arr) {
return Promise.reject(new Error('Implement this function'));
}
// The below are tests to help you check your `race()` implementation
test1().then(test2).then(() => console.log('Done!')).
catch(error => console.error(error.stack));
function test1() {
const p1 = new Promise(resolve => setTimeout(() => resolve(1), 10));
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const f = v => { if (v !== 1) throw Error('test1 failed!'); };
return race([p1, p2]).then(f);
}
function test2() {
const error = new Error('Expected error');
const p1 = new Promise(resolve => setTimeout(() => resolve(1), 100));
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const p3 = new Promise((resolve, reject) => reject(error));
return race([p1, p2, p3]).then(
() => { throw Error('test2: `race()` promise must reject'); },
e => { if (e !== error) throw Error('test2: wrong error'); });
}
Async/Await Internals 28
Async/Await Internals
Promises are the fundamental tool for integrating with async/await. Now that you've seen how
promises work from the ground up, it's time to go from the micro to the macro and see what
happens when you await on a promise. Even though async functions are at like synchronous
functions, they're as asynchronous as the most callback-laden banana code under the hood.
As you might have already guessed, await makes JavaScript call then() under the hood.
Example 3.1
const p = {
then: onFulfilled => {
// Prints "then(): function () { [native code] }"
console.log('then():', onFulfilled.toString());
// Only one entry in the stack:
// Error
// at Object.then (/examples/chapter3.test.js:8:21)
console.log(new Error().stack);
onFulfilled('Hello, World!');
}
};
The await keyword causes JavaScript to pause execution until the next iteration of the event
loop. In the below code, the console.log() after the await runs after the ++currentId code,
even though the increment is in a callback. The await keyword causes the async function to
pause and then resume later.
Example 3.2
const startId = 0;
let currentId = 0;
process.nextTick(() => ++currentId);
const p = {
then: onFulfilled => {
console.log('then():', currentId - startId); // "then(): 1"
onFulfilled('Hello, World!');
}
};
29
Notice that the then() function runs on the next tick, even though it is fully synchronous. This
means that await always pauses execution until at least the next tick, even if the thenable is not
async.The same thing happens when the awaited promise is rejected. If you call
onRejected(err) , the await keyword throws err in your function body.
Example 3.3
const startId = 0;
let currentId = 0;
process.nextTick(() => ++currentId);
const p = {
then: (onFulfilled, onRejected) => {
console.log('then():', currentId - startId); // "then(): 1
return onRejected(Error('Oops!'));
}
};
try {
console.log('Before:', currentId - startId); // "Before: 0"
await p;
console.log('This does not print');
} catch (error) {
console.log('After:', currentId - startId); // "After: 1"
}
await vs return
Recall that return in an async function resolves the promise that the async function returns. This
means you can return a promise. What's the difference between await and return ? The
obvious answer is that, when you await on a promise, JavaScript pauses execution of the async
function and resumes later, but when you return a promise, JavaScript nishes executing the
async function. JavaScript doesn't "resume" executing the function after you return .
The obvious answer is correct, but has some non-obvious implications that tease out how await
works. If you wrap await p in a try/catch and p is rejected, you can catch the error. What
happens if you instead return p ?
Example 3.4
30
Notice that try/catch does not catch the rejected promise that you returned. Why does only
await give you a catchable error when the promise is rejected? Because await throws the error
when it resumes execution. When you return a promise, JavaScript stops executing your async
function body and kicks off the resolve() process on the async function promise.
On the other hand, when you await on a promise, JavaScript pauses executing your async
function and resumes once the promise is settled. When JavaScript resumes your async function
after await , it throws an error if the awaited promise rejected. Below is a ow chart showing what
happens when you await on a promise.
On the other hand, when you return a promise from an async function, your promise goes into
the JavaScript runtime and never goes back into your code, so try/catch won't handle the error
in example 3.4. Below are a couple alternatives that catch the error: example 3.5 assigns await
p to a variable v and then returns the variable, and example 3.6 uses return await .
Example 3.5
31
Both approaches work, but example 3.5 is simpler and less confusing. Seeing return await is a
head-scratcher for engineers that aren't JavaScript experts, and that's antithetical to the goal of
making asynchronous code easy for average developers.
Concurrency
So far, you've seen that await p makes JavaScript pause your async function, call p.then() , and
resume once the promise is settled. What does this mean for running multiple async functions in
parallel, especially given that JavaScript is single threaded?
The "JavaScript is single threaded" concept means that, when a normal JavaScript function is
running, no other JavaScript can run. For example, the below code will never print anything. In
other languages, a construct like setImmediate() may run logic in a separate thread and print
even while an in nite loop is spinning, but JavaScript does not allow that.
Example 3.7
JavaScript functions are like the Pauli Exclusion Principle in physics: no two normal JavaScript
functions can be running in the same memory space at the same time. Closures (callbacks) are
separate functions, so in the below example, foo() , bar() , and baz() all run separately.
Example 3.8
function foo() {
let x = 0;
// When `foo()` is done, `bar()` will run later but still have
// access to `x`
setImmediate(bar);
// Stop running `foo()` until `baz()` is done
baz();
function bar() {
++x;
}
function baz() {
++x;
}
}
32
Async functions follow the same rule: no two functions can be running at the same time. But, any
number of async functions can be paused at the same time as long as you don't run out of
memory, and other functions can run when an async function is paused.
Example 3.9
This makes async functions useful for breaking up long-running synchronous functions. For
example, suppose you want to run two functions in parallel that each compute a large Fibonacci
number. Without async/await, you'd need tricky recursion. Async/await makes this task trivial.
Example 3.10
This example is simple but contrived. A more realistic example would be an Express API endpoint
that runs a potentially expensive algorithm like clustering. I have used this pattern in a production
Express API to run an O(n^5) clustering algorithm in a route without blocking other routes.
The key takeaway here is that an async function will run with no interruptions unless you pause it
with await or exit the function with return or throw . JavaScript is still single threaded in the
conventional sense, so two async functions can't be running at the same time, but you can pause
your async function using await to give the event loop and other functions a chance to run.
Async/Await vs Generators 33
Async/Await vs Generators
Async/await has a lot in common with generators, a feature that JavaScript introduced in the
2015 edition of the language spec. Like async functions, generator functions can be paused and
later resumed. There are two major differences between generator functions and async functions:
1. The keyword you use to pause a generator function is yield , not await .
2. When you pause a generator function, control goes back to your JavaScript code, rather than
the JS interpreter. You resume the generator function by calling next() on a generator object.
The below example demonstrates using yield to pause the generator and next() to resume it.
Example 3.11
With the help of a library, generators support a pattern virtually identical to async/await. The most
popular generator concurrency library is co. Here's example 1.1 with co instead of async/await.
Example 3.12
const co = require('co');
// `co.wrap()` converts a generator into an async-like function
const runCo = co.wrap(function*() {
// This function will print "Hello, World!" after 1 second.
yield new Promise(resolve => setTimeout(() => resolve(), 1000));
console.log('Hello, World!');
});
// In particular, wrapped functions return a promise
runCo().catch(error => console.log(error.stack));
34
Co offers several neat features that async/await does not natively support. By virtue of being a
userland library, co can be more extensible. For example, co can handle when you yield an array
of promises or a map of promises.
Example 3.13
The ip-side of co's implicit promise conversion is that co throws an error if you yield something
that it can't convert to a promise.
Example 3.14
In practice, co treating yield 1 as an error helps catch a lot of errors, but also causes a lot of
unnecessary errors. With async/await, await 1 is valid and evaluates to 1 , which is more robust.
Async/await has a few other advantages over co and generators. The biggest advantage is that
async/await is built-in to Node.js and modern browsers, so you don't need an external library like
co. Async/await also has cleaner stack traces. Co stack traces often have a lot of
generator.next() and onFulfilled lines that obscure the actual error.
Example 3.15
35
The equivalent async/await stack trace has the function name and omits generator.next() and
onFulfilled . Async/await's onFulfilled runs in the JavaScript interpreter, not userland.
Example 3.16
In general, async/await is the better paradigm because it is built in to JavaScript, throws fewer
unnecessary errors, and has most of the functionality you need. Co has some neat syntactic sugar
and works in older browsers, but that is not enough to justify including an external library.
Core Principles
So far, this chapter has covered the technical details of what it means for an async function to be
paused. What does all this mean for a developer looking to use async/await for their application?
Here's some core principles to remember based on the behaviors this chapter covered.
Just because you can await 1 doesn't mean you should. A lot of async/await beginners abuse
await and await on everything.
Example 3.17
In general, you should use await on a value you expect to be a promise. There is no reason to
await on a value that will never be a promise, and it falsely implies that the value may be a
promise. If a function can be synchronous, it should be synchronous.
The only reason to make the findSubstr() function async would be to pause execution and let
other functions run like in example 3.10. This is only potentially bene cial if findSubstr() runs
on a massive array. In that case, you should use await new Promise(setImmediate) in order to
make sure all other tasks have a chance to run.
36
Similarly, you must convert any value you want to await on into a promise. For example, if you
want to await on multiple promises in parallel you must use Promise.all() .
Example 3.18
As demonstrated in example 3.4, you can return a promise from an async function, but doing so
has some nuances and corner cases. Instead of using a promise as the resolved value, use await
to resolve the value and then return the value. It is generally easier to use await and return the
resolved value than to explain the difference between async and return .
Example 3.19
Use loops rather than array helpers like forEach() and map() with await
Because you can only await in an async function, async functions behave differently than
synchronous functions when it comes to functional array methods like forEach() . For example,
the below code throws a SyntaxError because await is not in an async function.
Example 3.20
37
You might think that all you need is an async arrow function. But that does not pause test() .
Example 3.21
Consolidated error handling is one of the most powerful features of async/await. Using .catch()
on an async function call lets you handle all errors (synchronous and asynchronous) that occur in
the async function. Use .catch() for catch-all error handlers rather than try/catch .
Example 3.22
In general, any error in an async function should end up in a .catch() handler. If you see
async/await based code with no .catch() calls, there's an unhandled error somewhere. Good
async/await code uses some centralized mechanism like a wrap() function to ensure every
async function call gets a .catch() at the end.
Example 3.23
Many JavaScript HTTP clients, like superagent, support a chainable API for building up requests
with function calls. Many ODMs and ORMs support a similar API for building database queries.
superagent.get(url).set('API-Key', 'test').
end((err, res) => { /* Handle response */ });
The below HTTPRequest class provides a simpli ed HTTP client with a chainable API, but
currently it only supports callbacks via the exec() function. Implement the then() function so
this HTTPRequest class works with async/await.
Below is the starter code. You may copy this code and complete this exercise in Node.js, or you
may complete it in your browser on CodePen at http://bit.ly/async-await-exercise-31 .
39
Exercise 2: Async forEach()
As shown in example 3.21, the forEach() array function has several quirks when it comes to
async/await:
Implement an async function forEachAsync() that takes an array and an async function fn() ,
and calls fn() on every element of the array in series. The forEachAsync() function should wait
for one instance of fn() to nish running before continuing on to the next one.
Below is the starter code. You may copy this code and complete this exercise in Node.js, or you
may complete it in your browser on CodePen at http://bit.ly/async-await-exercise-32 .
Now that JavaScript has features like async/await, these libraries and frameworks are even more
powerful. In this chapter, you'll see how async/await interacts with several common npm
packages. In addition, you'll learn to evaluate whether a package works with async/await.
Broadly speaking, npm packages belong to one of two categories when it comes to integrating
with async/await: libraries and frameworks.
Generally, when working with a framework, like Express or Redux, you pass functions to the
framework that the framework then calls for you.
Conversely, a library, like superagent or the MongoDB driver, exposes a collection of functions
for you that you're responsible for calling.
Not all npm packages fall neatly into one of these categories. But, these categories help break the
question of whether a given package "works" with async/await down into two easier questions.
For a framework to support async/await, it must support functions that return promises.
Example 4.1
Now let's apply these principles to several popular npm packages, starting with the test
framework mocha.
With Mocha 41
With Mocha
Mocha falls rmly into the framework category. It's a framework that runs behavior-driven
development (BDD) tests for you. The below example is from the Mocha home page. It has one
test that asserts that JavaScript's built-in indexOf() function handles a simple case correctly.
Example 4.3
The describe() calls are analogous to test suites in more conventional testing frameworks like
JUnit, and the it() calls are individual tests. So Mocha's async/await support is contingent on
whether the it() function supports passing in a function that returns a promise.
To gure out whether Mocha supports promises, go to their documentation site, which has a
section on promises pictured below.
So Mocha does support async/await as a framework. Digging deeper, it turns out Mocha has
enjoyed rudimentary promise support since v1.8.0 in March 2014.
42
Below is an example of using Mocha with an async function.
Example 4.4
describe('async', function() {
it('works', async function() {
assert.equal(await Promise.resolve(42), 42);
});
});
With Express
Express is a Node.js web framework used for building HTTP servers, like RESTful APIs and classic
web applications. The key term here is that Express is primarily a framework, which means its
async/await support is predicated on supporting functions that return promises. Below is an
example showing how to use Express with synchronous functions.
Example 4.5
Since Mocha supports async/await out of the box, you might mistakenly assume that Express
supports async/await too. That would be a mistake. However, it is an easy mistake to make
because the below code works ne, even though the Express route handler function is now async.
Example 4.6
Figuring out that Express doesn't fully support async/await is tricky because they don't explicitly
say one way or the other in the docs. If you Google "express async/await", you'll end up at an old
GitHub issue that's still open and implies that promises are not quite supported.
43
Unfortunately, this GitHub issue isn't explicit about where the interaction between Express and
async/await breaks down. The issue is what happens when your async function throws an error.
Example 4.7
In older versions of Node.js, the superagent request above will hang. In newer versions of Node.js,
the Express server process will crash because Express does not handle errors in promises.
Unfortunately, there is no way to make Express handle promises correctly without monkey-
patching Express itself or using a wrapper function. Using a wrapper function is the better choice,
because it is di cult to foresee all the potential consequences of replacing part of a framework's
code. Below is an example of a wrapper function you can use to handle async function errors with
Express.
Example 4.8
44
Error handling often causes async/await integration issues. Make sure to check whether
frameworks you use handle errors in async function correctly. Express is not the only framework
that seems to support async functions at rst glance but does not handle errors.
With MongoDB
Mocha is an example of a framework that fully supports async functions and Express is an
example of a framework that does not support async functions. Let's take a look at an example of
a Node.js library: the o cial MongoDB driver for Node.js.
The MongoDB driver generally does not execute functions for you, with a few exceptions like
callbacks. Apps built on the MongoDB driver primarily use the driver's functions for CRUD (create,
read, update, delete) operations:
Example 4.9
For a library to support async/await, its functions must return thenables. The documentation
shows that functions like insertOne() return a promise, as long as you don't specify a callback.
45
This means the MongoDB driver supports async/await from a library perspective. However, using
the MongoDB driver with async/await lets you do more than just await on individual CRUD
operations. Async/await opens up some elegant alternatives for streaming data using for loops.
Most database applications only read a few documents from the database at a time. But what
happens if you need to read through millions of documents, more than can t into your
application's memory at one time? The MongoDB driver has a construct called a cursor that lets
you iterate through huge data sets by only loading a xed number of documents into memory at
any one time.
Fundamentally, a MongoDB cursor is an object with a function next() that returns a promise
which resolves to the next document, or null if there are no more documents. Without
async/await, iterating through a cursor using next() required recursion. With async/await, you
can iterate through a cursor using a for loop:
Example 4.10
await db.collection('Movie').insertMany([
{ title: 'Star Wars', year: 1977 },
{ title: 'The Empire Strikes Back', year: 1980 },
{ title: 'Return of the Jedi', year: 1983 }
]);
// Do not `await`, `find()` returns a cursor synchronously
const cursor = db.collection('Movie').find();
for (let v = await cursor.next(); v != null; v = await cursor.next()) {
console.log(v.year); // Prints "1977", "1980", "1983"
}
That's right, you can await within a for loop's statements. This pattern is a more intuitive and
performant way to iterate through a cursor than using recursion or streams.
With Redux
React is the most popular JavaScript UI framework, and Redux is the most popular state
management framework for React. The two have become largely synonymous since Redux's
release in 2015. For the purposes of async/await integration, both React and Redux are
frameworks.
First, let's look at how to integrate Redux with async/await. Below is an example of using Redux
with synchronous functions in vanilla Node.js. Redux has 3 primary concepts: stores, actions, and
reducers. A store tracks the state of your application, an action is an object representing some
change going through the system, and a reducer is a synchronous function that modi es the
application state object in response to actions.
46
Example 4.11
Redux beginners might be wondering why you need to dispatch actions rather than modifying the
state directly using the assignment operator. Watching for changes on a JavaScript value is hard,
so actions exist to make it easy to observe all changes going through the system. In particular,
Redux makes it easy to update your React UI every time your state changes.
So can you use async/await with Redux? Redux reducers must be synchronous, so you cannot
use an async function as a reducer. However, you can dispatch actions from an async function.
Example 4.12
Calling store.dispatch() from an async function works, but doesn't toe the Redux party line.
The o cial Redux approach is to use the redux-thunk package and action creators. An action
creator is a function that returns a function with a single parameter, dispatch .
47
Example 4.13
redux-thunk 's purpose is inversion of control (IoC). In other words, an action creator that takes
dispatch() as a parameter doesn't have a hard-coded dependency on any one Redux store. Like
AngularJS dependency injection, but for React.
With React
Redux is best with React, the most popular UI framework for JavaScript. To avoid bloat, this
chapter will not use JSX, React's preferred extended JS syntax. Below is an example of creating a
component that shows "Hello, World!" in React:
Example 4.14
Currently, render() functions cannot be async. An async render() will cause React to throw a
"Objects are not valid as a React child" error. The upcoming React Suspense API may change this.
React components have lifecycle hooks that React calls when it does something with a
component. For example, React calls componentWillMount() before adding a component to the
48
DOM. The below script will also generate HTML that shows "Hello, World!" because
componentWillMount() runs before the rst render() .
Example 4.15
The componentWillMount() hook does not handle async functions. The below script produces
an empty <h1> .
Example 4.16
In general, React doesn't handle async functions well. Even though async
componentWillMount() works in the browser, React won't handle errors and there's no way to
.catch() . To use async functions with React, you should use a framework like Redux. The
following is an example of using Redux action creators with React.
49
Example 4.18
Unfortunately, redux-thunk doesn't handle errors in async action creators for you. But, since the
action creator is a function as opposed to a class method, you can handle errors using a wrapper
function like in Example 4.8. For React and Redux, the wrapper function should dispatch() an
error action that your UI can then handle.
Example 4.19
For frameworks that lack good async/await support, like React, Redux, and Express, you should
use wrapper functions to handle errors in a way that makes sense for the framework. Be wary of
plugins that monkey-patch the framework to support async/await. But, above all, make sure you
handle errors in your async functions, because not all frameworks will do that for you.
Schedules a job to run name once at a given time. when can be a Date or a String such as
tomorrow at 5pm .
data is an optional argument that will be passed to the processing function under
job.attrs.data .
cb is an optional callback function which will be called when the job has been persisted in the
database.
agenda.schedule('tomorrow at noon', [
'printAnalyticsReport',
'sendNotifications',
'updateUserRecords'
]);
The key function for interacting with a WebSocket is the onmessage function, which JavaScript
calls for you when the socket has new data to process.
JavaScript does not handle errors that occur in onmessage if onmessage is async. Write a
function that wraps async onmessage functions and logs any errors that occur to the console.
Below is the starter code. You may copy this code and complete this exercise in Node.js, although
you will need the isomorphic-ws npm module because Node.js does not have WebSockets. You
may complete also complete it in your browser on CodePen at http://bit.ly/async-await-
exercise-42 .
Moving On 52
Moving On
Async/await is an exciting new tool that makes JavaScript much easier to work with. Async/await
won't solve all your problems, but it will make your day-to-day easier by replacing callbacks and
promise chaining with for loops and if statements. But remember the fundamentals, or you'll
end up trading callback hell and promise hell for async/await hell. Here are some key points to
remember when working with async/await.
To get more content on async/await, including design patterns and tools for integrating with
popular frameworks, check out my blog's async/await section at bit.ly/async-await-blog .
Congratulations on completing this book, and good luck with your async/await coding adventures!
53