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.
This guide applies to both front end JavaScript and node. Remember async/await is supported in:
util.promisify(), allowing the node stdlib to return Promises and thus be used with await.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"
]
}
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.
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:
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');
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}`)
}
This often means turning examples using .then() syntax into async/await versions. Here's some handy examples:
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']
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();
Using await in a function tends to make functions that call that code flatter too. For example:
= await to GET the resource= await to relax until that promise has a value, omitting the callback.You'll notice await tends to creep down the stack:
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:
done callbackErrors will be caught by Mocha.Essentially, async unit testing with await looks like synchronous unit testing.
Luckily we've written a dedicated async/await troubleshooting guide with a bunch of useful tips.
The most important thing to remember during an await refactor is:
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.
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!