Skip links

Simplify your promises with async/await: four examples

Node.js version 8.9, released in fall 2017, is the first LTS (long time support) version including ECMAScript 2017 support. This is an opportunity to simplify the use of promises thanks to the asynchronous functions and both async await keywords: your code will be simpler, more readable, and maintainable. In short: clean.

Node.js, as often with JavaScript, relies on asynchronous operations to manage concurrency, especially in the case of I/O. Historically, these asynchronous operations were managed using callbacks, which required a significant effort from the developers to avoid the famous callback hell :

				
					aCb(function(err, resultFromA) {
    if (err) {
        return handleError(err);
    }
    bCb(resultFromA, function(err, resultFromB) {
        if (err) {
            return handleError(err);
        }
        cCb(resultFromB, function(err, resultFromC) {
            if (err) {
                return handleError(err);
            }
            doSomething(resultFromC);
        });
    });
});
				
			

With the promises supported by Node.js since version 0.12, it is possible to drastically simplify asynchronous operations with a redesign and better error handling :

				
					aPr().then(function(resultFromA) {
    return bPr(resultFromA);
}).then(function(resultFromB) {
    return cPr(resultFromB);
}).then(function(resultFromC) {
    doSomething(resultFromC);
}).catch(function(err) {
    handleError(err);
})
				
			

The example is eloquent: promises simplify the code, and error handling is no longer duplicated. Asynchronous functions bring the concept of promises even further by adding syntactic sugar that makes the code more concise and avoids some of the shortcomings of promises. Let’s demonstrate this with four examples.

Asynchronous functions

An asynchronous function is defined with the keyword async , before a normal function (async function m(args) { … } ) or an arrow function (async (args) => … ), and allows the use of the await operator. This operator is used before an expression. If the result of the evaluation of the expression is a promise, the function’s execution is interrupted until the promise is finished (it is said to be settled, acknowledged). In case the promise is resolved, the await operator returns the resolved value. Otherwise, the promise is rejected, and an exception is thrown. If the result of the evaluation of the await expression is not a promise, it is simply returned synchronously.

The previous example can therefore be rewritten in this way:

				
					async function m() {
    try {
        const resultFromA = await aPr();
        const resultFromB = await bPr(resultFromA);
        const resultFromC = await cPr(resultFromB);
        doSomething(resultFromC);
    } catch (err) {
        handdleError(err);
    }
}
				
			

Here, the execution of the function m is interrupted after the call to aPr , and resumes when the promise returned by aPr has been resolved. The resolved value is then assigned to resultFromA. We thus obtain a code that executes asynchronous functions with a syntax close to the one used for synchronous calls. Note that the functions aPrbPr, and cPr, are the same as in the previous example and return promises. Unlike the switch from callbacks to promises, asynchronous functions are not a paradigm shift and can be adopted gradually or partially.

An asynchronous function itself returns a promise, which is resolved when the function’s execution is completed, along with the value eventually returned. Note that the await operator is implicit in the case of return, so “return await p;” is equivalent to “return p;”.

Four examples to understand the benefits of asynchronous functions 

One of the main advantages of asynchronous functions is obvious: the code is more concise and cleaner. There is no more chaining of calls to then and function declarations. But there are many other advantages to using asynchronous functions, and I will illustrate four of them here.

Synchronous and asynchronous errors

When using the classic promise syntax, there are two types of errors to handle: errors produced by synchronous calls and promises. With await, promises are used as synchronous calls, including error handling. Let’s take the following code :

				
					function mPr() {
    try {
        const resFromA = a(); // opération synchrone pouvant lever une exception
        bPr(resFromA).then(function(resFromB) {
            … // opération asynchrone pouvant lever une exception
        }).catch(function(err) {
            handdleError(err);
        });
    } catch (err) {
        handdleError(err);
    }
}
				
			

To catch an exception possibly raised by the synchronous call to a, you must use a try/catch. To catch an exception possibly raised by the asynchronous call to bPr, you have to use the catch method. With an asynchronous function, only try/catch is used :

				
					async function mAs() {
    try {
        const resFromA = a();
        const resFromB = await bPr(resFromA);
    } catch (err) {
        handdleError(err);
    }
}
				
			

Calls in a loop

In the case where interdependent asynchronous operations have to be executed in a loop, the promises force recursive calls which are not easy to understand and very difficult to maintain :

				
					function r(val) {
    return aPr(val).then(function(res) {
        return shouldContinue(res) ? r(res) : res;
    });
}
				
			

Here, the chain of promises is implicit, and this function is far too complex for its size. With an asynchronous function, the loop becomes explicit:

				
					async function r(val) {
    while (shouldContinue(val)) {
        val = await aPr(val);
    }
}
				
			

These functions are equivalent, but you need a good knowledge of promises to understand the first one.

Be careful: if your synchronous operations are independent, it is better to create the promises simultaneously and use Promise.all to execute them in parralel.

Intermediate values

It is sometimes necessary to keep the results of several chained promises. Different solutions exist to solve this problem, but all have their drawbacks. For example, you can store the intermediate values in mutable variables of the closure :

				
					function mPr1() {
    let resultFromA;
    aPr().then(function(_resultFromA) {
        resultFromA = _resultFromA;
        return bPr(resultFromA);
    }).then(function(resultFromB) {
        doSomething(resultFromA, resultFromB);
    });
}
				
			

Even cleaner, we can also rely on Promise.all to return several values :

				
					function mPr2() {
    aPr().then(function(resultFromA) {
        return Promise.all([resultFromA, bPr(resultFromA)]);
    }).then(function([resultFromA, resultFromB]) {
        doSomething(resultFromA, resultFromB);
    });
}
				
			

With the asynchronous functions, you avoid any mental gymnastics :

				
					async function mAs() {
    const resultFromA = await aPr();
    const resultFromB = await bPr(resultFromA);
    doSomething(resultFromA, resultFromB);
}
				
			

Conditional calls

It happens that some asynchronous operations are conditional. In this case, it is no longer possible to flatten them, and we end up with nested promises :

				
					function mPr() {
    aPr().then(function(resultFromA) {
        if (needMore(resultFromaA)) {
            return bPr(resultFromA).then(function(resultFromB) {
                doSomething(resultFromA, resultFromB);
            });
        } else {
            doSomething(resultFromA);
        }
    });
}
				
			

This nesting is reminiscent of the problems of using callbacks. With asynchronous functions, we can reduce the depth of the function :

				
					async function mAs() {
    const resultFromA = await aPr();
    if (needMore(resultFromaA)) {
        const resultFromB = await bPr(resultFromA);
        doSomething(resultFromA, resultFromB);
    } else {
        doSomething(resultFromA);
    }
}
				
			

Conclusion

Asynchronous functions with the await operator rely on promises to offer a clearer syntax. This makes the code cleaner, which is important for asynchronous operations that always require more thought.

If you already use promises, the transition to asynchronous functions is smooth. So don’t hesitate to use them for your future developments, or even to rewrite certain portions of your code during maintenance, in order to respect the boy scout rule: always leave the code cleaner than when you arrived.