I’ve been using async/await, with help from babel, for the past several months at work and I figured I’d share some patterns I’ve found beneficial. I don’t have a background with async/await in other languages so I am coming at this problem without previous expertise and/or baggage.
I also gave this post in the form of a talk. Slides can be found here: https://chrishiestand.github.io/2016-01-05-js-async-await/#/
TL;DR
The big benefit of using async/await is that it flattens and shortens (altogether simplifying) your code. I believe that async/await is a big win over plain promises. Adoption by the community will lead to cleaner, easier to read and maintain, code.
- Return promises (or use async functions) everywhere you do non-streaming asynchronous calls
- Don’t forget
process.on('unhandledRejection', ...)
- Minimize the number of remote calls per function and use promise management utilities to keep sanity.
- For performance, start a promise but defer the
await
until other work is done. let [value1, value2, value3] = await Promise.all([p1, p2, p3])
brings order to chaossleep(x)
≅await _Promise.delay(x)
If the executive summary isn’t enough, please read on.
Unhandled Promise Rejections
Unhandled Promise Rejection Anti-Example
Warning: At time of writing, nodejs will silently drop any rejected promises (that are being awaited). In this example there is no server listening on port 1337 and this promise will silently fail.
note: I suffix functions that return promises with _P
"use strict";
import _Fetch from 'isomorphic-fetch';
// We intentionally are *not* running an http server on port 1337
function fetch_P() {
return _Fetch('http://localhost:1337/servernotrunning.json')
.then((result) => {
return result.json();
});
}
async function main() {
let data = await fetch_P();
console.log('got data');
}
// This will fail silently
main();
Unhandled Promise Rejection Example
To workaround this potentially deadly problem, we have to put this little tidbit after our imports. I put this tidbit at the top of all library files containing promises entrypoint files using async/await or async/await libraries.
To read more about this: https://github.com/nodejs/promises/issues/26
"use strict";
import _Fetch from 'isomorphic-fetch';
// workaround for hidden promise rejections, see: <https://github.com/nodejs/promises/issues/26>
process.on('unhandledRejection', err => { throw err; });
// We intentionally are *not* running an http server on port 1337
function fetch_P() {
return _Fetch('http://localhost:1337/servernotrunning.json')
.then((result) => {
return result.json();
});
}
async function main() {
let data = await fetch_P();
console.log('got data');
}
// This will fail but will at least an error will be thrown
main();
With the unhandledRejection
listener, any failed awaits which do not have an error handler will be fatal errors. If you do not want these to be fatal errors, as uncaught exceptions usually are, then you should use either a try/catch block around the await or the standard promise error handling style: await fetch_P().catch(...)
Build with Promises, Execute with Await
At this point, I’m not sure why anybody would be using callbacks, or anything else, for singular async responses (seriously, is there a reason to use anything else?). Use promises everywhere. Node supports native promises and A+ compatible libraries like bluebird can be used for additional utility functions.
If you’ve already been writing code with promises then you’re smart - async/await works beautifully with your promises out of the box.
Async with Promises
function main() {
//Initializing an empty variable to ensure lexical/thenable scope always felt awkward
let data;
// fetch_P() is called first
fetch_P()
.then((result) => {
// This happens third
data = result;
console.log('got data');
});
// This happens second
console.log("shouldn't I have the data here? I mean, this code *is* further down the screen.");
}
Async with async/await
async function main() {
// This happens first
let data = await fetch_P();
// This happens second
console.log('got data');
// This happens third
console.log('sanity restored');
}
Inefficient promise anti-example
Try to minimize the number of remote calls being done in each promise or async function.
// fetch_P returns a promise
async function fetch_P(auth, key) {
// anti-pattern: an unnecessary additional
// remote call in a function
let config = await getConfig_P(auth);
return getValue_P(config, key);
}
async function main(auth) {
let value1 = fetch_P(auth, 'key1'); // gets config
let value2 = fetch_P(auth, 'key2'); // gets config too
let value3 = fetch_P(auth, 'key3'); // gets config three
}
Minimum waiting per function example
Preferably, our library code does not entail unnecessary waiting. That way the higher level code can implement parallelism.
function fetch_P(config, key) {
// This just returns a promise and does not wait
return getValue_P(config, key);
}
async function main(auth) {
let config = await getConfig_P(auth);
// These promises execute in parallel
let p1 = fetch_P(config, 'key1');
let p2 = fetch_P(config, 'key2');
let p3 = fetch_P(config, 'key3');
// This only executes after all 3
// parallel promises are settled
// assignment is stable: order is retained
let [value1, value2, value3] = await Promise.all([p1, p2, p3]);
}
Javascript can sleep
Sometimes, particularly in testing, it is not simple to return a promise or trigger a callback precisely after a decoupled remote operation has completed. Here’s a quick shortcut that helps you to wait until a remote operation should be complete.
import Assert from 'assert';
import _Promise from 'bluebird';
async function test() {
decoupledRemoteThing();
// decoupledRemoteThing should be complete after 1 second
// Akin to `sleep(1000)` in synchronous languages
await _Promise.delay(1000);
// This runs at least 1 second after decoupledRemoteThing() was executed
let remoteValue = await getRemoteValue_P();
Assert(remoteValue === true);
}
Background async requests and defer waiting
Sometimes you can start a remote operation that you don’t need complete until later. Now it is easy to defer waiting until later - often meaning that the promise has already settled in the meantime and by the time the waiting code is called, no waiting is actually done.
async function main() {
let inputQueue = await getIntputQueue_P();
// Start this operation now, but we will use the result later; note the lack of `await`
let outputQueueDefer = getOutputQueue_P();
// This may take some time
let workDone = await doWork_P(inputQueue);
// By the time doWork_P() is done, this promise is probably already resolved
let outputQueue = await outputQueueDefer;
await putResult_P(outputQueue, workDone);
}
note: In the original, having not thought of a better term, I used the term “blocking” to describe when the javascript engine would wait for asynchronous code, but I did not mean blocking as in blocking the thread. I’ve changed the phrase to “waiting” to be more clear.
Comments
comments powered by Disqus