Date
Node JS

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 chaos
  • sleep(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