Before we get into developing our application, we must take a deeper look at a pair of new ES-2015/2016/2017 features that collectively revolutionize JavaScript programming: The Promise class and async functions. Both are used for deferred and asynchronous computation and can make intensely nested callback a thing of the past:
- A Promise represents an operation that hasn't completed yet but is expected to be completed in the future. We've seen Promises in use. The .then or .catch functions are invoked when the promised result (or error) is available.
- Generator functions are a new kind of function that can be paused and resumed, and can return results from the middle of the function.
- Those two features were mixed with another, the iteration protocol, along with some new syntax, to create async functions.
The magic of async functions is that we can write asynchronous code as if it's synchronous code. It's still asynchronous code, meaning long-running request handlers won't block the event loop. The code looks like the synchronous code we'd write in other languages. One statement follows another, the errors are thrown as exceptions, and the results land on the next line of code. Promise and async functions are so much of an improvement that it's extremely compelling for the Node.js community to switch paradigms, meaning rewriting legacy callback-oriented APIs.
Over the years, several other approaches have been used to manage asynchronous code, and you may come across code using these other techniques. Before the Promise object was standardized, at least two implementations were available: Bluebird (http://bluebirdjs.com/) and Q (https://www.npmjs.com/package/q). Use of a non-standard Promise library should be carefully considered, since there is value in maintaining compatibility with the standard Promise object.
The Pyramid of Doom is named after the shape the code takes after a few layers of nesting. Any multistage process can quickly escalate to code nested 15 levels deep. Consider the following example:
router.get('/path/to/something', (req, res, next) => {
doSomething(arg1, arg2, (err, data1) => {
if (err) return next(err);
doAnotherThing(arg3, arg2, data1, (err2, data2) => {
if (err2) return next(err2);
somethingCompletelyDifferent(arg1, arg42, (err3, data3) => {
if (err3) return next(err3);
doSomethingElse((err4, data4) => {
if (err4) return next(err4);
res.render('page', { data });
});
});
});
});
});
Rewriting this as an async function will make this much clearer. To get there, we need to examine the following ideas:
- Using Promises to manage asynchronous results
- Generator functions and Promises
- async functions
We generate a Promise this way:
exports.asyncFunction = function(arg1, arg2) {
return new Promise((resolve, reject) => {
// perform some task or computation that's asynchronous
// for any error detected:
if (errorDetected) return reject(dataAboutError);
// When the task is finished
resolve(theResult);
});
};
Your code must indicate the status of the asynchronous operation via the resolve and reject functions. As implied by the function names, reject indicates an error occurred and resolve indicates a success result. Your caller then uses the function as follows:
asyncFunction(arg1, arg2)
.then((result) => {
// the operation succeeded
// do something with the result
return newResult;
})
.catch(err => {
// an error occurred
});
The system is fluid enough that the function passed in a .then can return something, such as another Promise, and you can chain the .then calls together. The value returned in a .then handler (if any) becomes a new Promise object, and in this way you can construct a chain of .then and .catch calls to manage a sequence of asynchronous operations.
A sequence of asynchronous operations would be implemented as a chain of .then functions, as we will see in the next section.