Last week saw the release of Node.js v7.6.0 which contained (amongst other things) an update to v8 5.5 (Node's underlying JS engine). This v8 release includes a brand new language feature: async functions. Utilising this new feature is done through 2 new keywords: async
and await
. Async functions are not new in the JS world and it has been possible to use them via transpilers (babel) for some time, but having them natively in the language means their use will become much more widespread.
So what is an async function?
Async functions makes promise-using code more readable than before. Now instead of using Promise.then()
to resolve your promise to a value, you can just prefix the promise with await
and your code magically pauses until the value is available, then proceeds with execution as if the value were synchronous all along. To allow this to happen, you need to use the async
keyword on the enclosing function.
Here's a before and after example of some code encrypting a password using bcrypt and updating a user's password to the hash:
Before:
schema.pre('save', function hashPassword(next) {
const user = this;
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
// generate a salt
return bcrypt.genSalt(10).then((salt) => {
// hash the password along with our new salt
return bcrypt.hash(user.password, salt).then((hash) => {
// override the cleartext password with the hashed one
user.password = hash;
return next();
}).catch(next);
}).catch(next);
});
And heres the same code as before rewritten to use async functions:
schema.pre('save', async function hashPassword(next) {
try {
const user = this;
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
// generate a salt
const salt = await bcrypt.genSalt(10);
// hash the password along with our new salt
const hash = await bcrypt.hash(user.password, salt);
// override the cleartext password with the hashed one
user.password = hash;
return next();
} catch (e) {
return next(e);
}
});
Note the async
keyword before the function, and await
when we need to resolve a promise to a value.
This code is flatter and easier to reason about. Error handling is performed using a single try..catch
block as opposed to adding .catch(next)
to the end of each promise.
Caveats
There are some caveats to using this approach. If you're not using promises everywhere already, then you'll need to wrap your calling code in a function then execute this function as a promise. Consider the following express route handler to fetch a user by id:
Before async functions:
app.get('/:id', (req, res, next) => {
User.findById(req.params.id).then(user => {
res.json(user)
}).catch(next)
})
Because express isn't promise-aware, you have to use a wrapping function to catch any errors and pass them onto next
:
app.get('/:id', (req, res, next) => {
async function action() {
const user = await User.findById(req.params.id)
res.json(user)
}
action().catch(next)
})
This is a little cleaner, but has the same level of nesting as before. This can be improved using a simple little helper function, to make express become promise-aware:
function asyncWrap(fn) {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
app.get('/:id', asyncWrap(async (req, res) => {
const user = await User.findById(req.params.id)
res.json(user)
})
This unifies the error handling to a single place and keeps it out of the route handler. If User.findById()
throws an error, it'll bubble up and get passed along to the error middleware as you'd expect.
To conclude
I think async/await
are game changers for the JS language. Although it's possible to write clean code using callbacks and promises, it can muddy the intent when it comes to error checking (either through repeated if (err) return cb(err)
or .catch(cb)
calls). Conversely however, async/await
makes it really easy to write code with lots of async operations which could cause performance issues and be signs of deeper architectural problems within your program.
Node.js 7.6.0 changelog: https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V7.md#2017-02-21-version-760-current-italoacasas
v8 5.5 changelog: https://v8project.blogspot.com/2016/10/v8-release-55.html