How to flatten JavaScript

Using 'await' instead of callbacks and .then()

By Mike on 10th Jul 2017, updated on 2nd Aug 2017

Since my previous article on async/await ended up getting popular I thought I'd follow up with more focused guide on flattening your JavaScript. We started this process on the CertSimple codebase a couple of weeks ago and while it was mostly straightforward, there were a couple of surprises.

The goal here is simple: remove callbacks and then(), simplifying the code.

This guide applies to both front end JavaScript and node. Remember async/await is supported in:

1. Install and configure eslint

jslint doesn't handle await yet. So install it's nerdier cousin, eslint. You'll probably also want eslint-must-use-await - which warns when callbacks or .then() is used. Note I'm biased here: I wrote eslint-must-use-await.

Your .eslintrc.json should look include these two sections:

{
    "parserOptions": {
        "ecmaVersion": 2017
    },
    "plugins": [
        "must-use-await"
    ]
}

2. For node: handle uncaught rejections

If a promise rejects - i.e., an error occurs - and you don't catch it, you'll see:

(node:14104) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: (some error here)
(node:14104) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

You won't get a traceback to find the offending line though. So add this to the first file in your app (eg, bin/www for an Express app):

process.on('unhandledRejection', function(reason, promise) {
    console.log(promise);
});

To get a full traceback like any other error.

3. Pick a small, isolated set of code

CertSimple uses various REST APIs for government company registrars, independent information sources, whois, DNS, Microsoft Cognitive Services and a bunch of our own logic. Each has a bunch of integration tests and mocks.

API clients are perfect for beginning your code flattening as they're:

4. Choose your weapons

To use await, you'll need your existing libraries to support Promises. The most common async task in an API client is HTTP requests, and you have a few options here:

node 8 includes util.promisify() that can be used to wrap older node libraries. So you can make a Promisified version of fs.readFile with:

const readFile = util.promisify(fs.readFile);

Then use that wrapped function - just await it and omit the callback.

const fileContents = await readFile('somefile.txt');

5. Modify your code to use async/await

The first step is simple:

superagent.get('/api/v1/some-resource', function(err, result){
    if ( err ) {
        // Handle err
        return
    }
    // Handle result...
});

Becomes:

var result = await superagent.get('/api/v1/some-resource');

You'll also need to add the async function to the parent scope (eslint will remind you about this with unexpected token).

You don't have to handle errors if you don't want to, but this is easily done. Since APIs break a lot, it's best that we do:

try {
    var result = await superagent.get('/api/v1/some-resource');
} catch(err) {
    log(`Oh no an error occured ${err.message}`)
}

6. Work out async/await equivalents of common asynchronous coding patterns.

This often means turning examples using .then() syntax into async/await versions. Here's some handy examples:

Doing a bunch of things in parallel and then doing something with the combined result:

var log = console.log;

var getBanana = function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve('banana')
        }, 2000);
    });
}

var getGrape = function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve('grape')
        }, 3000);
    });
}

var start = async function(){
    var result = await Promise.all([getBanana(), getGrape(), 'apple'])
    console.log('All done', result)
}

start()

Will log ['banana', 'grape', 'apple']

Handling _.debounce() with await

var log = console.log.bind(console),
    _ = require('lodash');

var resultsAfterDelay = new Promise(function(resolve) {
    _.debounce(function(){
        resolve('foo');
    }, 100)();
});

var start = async function(){
    log(await resultsAfterDelay)
}

start();

7. Modify your existing code and unit tests to call the new function signatures.

Using await in a function tends to make functions that call that code flatter too. For example:

You'll notice await tends to creep down the stack:

After you modify a function to await promises internally, that function itself must be awaited itself

A lot of editors and command line tools can properly find all instances of a function being called, but the most basic tool you can use is git grep.

For unit tests - we use mocha - changing from callbacks to await means you can simply:

Essentially, async unit testing with await looks like synchronous unit testing.

8. Handle common mistakes

Luckily we've written a dedicated async/await troubleshooting guide with a bunch of useful tips.

That's all for now.

The most important thing to remember during an await refactor is:

These things take time.

So your API client is nice and flat. But the thing that's calling it via await is still using callbacks. That's a reasonable intermediate step - these things take time.

We've been approaching flattening our code Joel style: before we work on new features, we'll clean any remaining callbacks in the code first. It's been going well so far, with the more popular code paths being largely flattened.

Mike MacCana, founder at CertSimple.

CertSimple makes EV HTTPS fast and painless.

An EV HTTPS certificate verifies the company behind your website. But getting verified is a slow painful process. CertSimple provides EV HTTPS certificates 40x faster than other vendors. We check your company registration, network details, physical address and flag common errors before you pay us, provide verification steps specific for your company, update in realtime during the process, and even check your infrastructure to help you set up HTTPS securely.
Verify your site now!