Node.js now supports Async/Await out-of-the-box since the version 7.6. If you haven’t tried and tested it then here are the main reasons for using it in place of Promises.
Here’s the quick intro about Async/Await:
- Async/await are the new options to write asynchronous codes, previously the asynchronous part was handled by Promises.
- Async/await are like Promises but built on top of it, i.e., they are non-blocking and they cannot be used with either plain or node callbacks.
- Async/await makes your code look-a-like of synchronous code.
// PROMISE IMPLEMENTATION const promiseMethod = () => getDataInJson() .then( data => { console.log(data) return "done" }) promiseMethod() // ASYNC/AWAIT IMPLEMENTATION const asyncAwaitMethod = async() => { console.log(await getDataInJson()) return "done" } asyncAwaitMethod()
Points of differences to note here are:
- Function has the keyword async before it.
- The await keyword can only be used inside functions defined with async.
- Any async function returns a promise implicitly, and the resolve value of the promise will be whatever you return from the function (which is the string “done” in our case).
The above points implies that we can’t use await in the top level of our code since that is not inside an async function.
// this will not work in top level // await requestMethod() // this will work requestMethod().then((result) => { // do something })
await getDataInJson()
means that the console.log call will wait until getDataInJson() promise resolves and print it value.
Now to the point of discussion, why Async/await is better than Promises:
- Concise and Clean:
Have you taken a look at how much code lines we have saved! Even in this very basic example above, it’s clear we have saved a decent amount of code. We didn’t have to write .then, create an anonymous function to handle the response, or give a name data to a variable that we don’t need to use. We also avoided nesting our code. These small advantages add up quickly and they will be more obvious in the following code examples.
- Error Handling
Async/await uses the good old try/catch
to handle both synchronous and asynchronous errors. In the example below with promises, the try/catch will not handle if JSON.parse fails because it’s happening inside a promise. We need to call .catch
on the promise and duplicate our error handling code, which will (hopefully) be more sophisticated than console.log in your production ready code.
const requestMethod = () => { try { getDataInJson() .then(result => { // this parse may fail const data = JSON.parse(result) console.log(data) }) // uncomment this block to handle asynchronous errors // .catch((err) => { // console.log(err) // }) } catch (err) { console.log(err) } }
Now let’s have a look at the same code with async/await. The catch block now will handle parsing errors.
const requestMethod = async () => { try { // this parse may fail const data = JSON.parse(await getDataInJson()) console.log(data) } catch (err) { console.log(err) } }
- Conditionals
Imagine something like the code below which fetches some data and decides whether it should return that or get more details based on some value in the data.
const requestMethod = () => { return getDataInJson() .then(data => { if (data.needsAnotherRequest) { return makeAnotherRequest(data) .then(moreData => { console.log(moreData) return moreData }) } else { console.log(data) return data } }) }
Just looking at this gives you a headache. It’s easy to get lost in all that nesting (n-levels), braces, and return statements that are only needed to propagate the final result up to the main promise.This example becomes way more readable when rewritten with async/await.
const requestMethod = async () => { const data = await getDataInJson() if (data.needsAnotherRequest) { const moreData = await makeAnotherRequest(data); console.log(moreData) return moreData } else { console.log(data) return data } }
- Intermediate values
You must have found yourself in a situation where you have to use Promise calls in a heirarchy where the return value of first promise is feed to second promise and then the results of both of these promises are used to call the third promise. Your code most likely looked like this
const requestMethod = () => { return promiseFirst() .then(valueFirst => { // do something return promiseSecond(valueFirst) .then(valueSecond => { // do something return promiseThird(valueFirst, valueSecond) }) }) }
If promiseThird didn’t require valueFirst it would be easy to flatten the promise nesting a bit. If you are the kind of person who couldn’t live with this, you could wrap both values of First & Second in a Promise.all
and avoid deeper nesting, like this
const requestMethod = () => { return promiseFirst() .then(valueFirst => { // do something return Promise.all([valueFirst, promiseSecond(valueFirst)]) }) .then(([valueFirst, valueSecond]) => { // do something return promiseThird(valueFirst, valueSecond) }) }
This approach sacrifices semantics for the sake of readability. There is no reason for valueFirst & valueSecond to belong in an array together, except to avoid nesting promises. This same logic becomes ridiculously simple and intuitive with async/await. It makes you wonder about all the things you could have done in the time that you spent struggling to make promises look less hideous.
const requestMethod = async () => { const valueFirst = await promiseFirst() const valueSecond = await promiseSecond(valueFirst) return promiseThird(valueFirst, valueSecond) }
- Error stacks
Imagine a piece of code that calls multiple promises in a chain, and somewhere down the chain an error is thrown.
const requestMethod = () => { return promiseCallMethod() .then(() => promiseCallMethod()) .then(() => promiseCallMethod()) .then(() => promiseCallMethod()) .then(() => promiseCallMethod()) .then(() => { throw new Error("oops"); }) } requestMethod() .catch(err => { console.log(err); // output // Error: oops at promiseCallMethod.then.then.then.then.then (index.js:8:13) })
The error stack returned from a promise chain gives no clue of where the error happened. Even worse, it’s misleading; the only function name it contains is promiseCallMethod which is totally innocent of this error (the file and line number are still useful though). However, the error stack from async/await points to the function that contains the error
const requestMethod = async () => { await promiseCallMethod() await promiseCallMethod() await promiseCallMethod() await promiseCallMethod() await promiseCallMethod() throw new Error("oops"); } requestMethod() .catch(err => { console.log(err); // output // Error: oops at requestMethod (index.js:7:9) })
On your local development machine, this might not be useful when you’re trying to make sense of error logs. But this will be quite useful for the error logs coming in from your productions servers. In such cases, knowing the error happened in requestMethod is better than knowing that the error came from a then after a then after a then …
- Debugging
One of the advantages of using async/await is that the code is much easier to debug. Debugging promises based code base has always been such a pain for 2 main reasons:
a. You cannot set breakpoints in arrow functions that return expressions (no body).
b. If you set a breakpoint inside a .then block and use debug shortcuts like step-over, the debugger will not move to the following .then because it only “steps” through synchronous code.
With async/await you don’t need arrow functions as much, and you can step through await calls exactly as if they were normal synchronous calls.
Conclusion
Async/await is one of the most revolutionary features that have been added to JavaScript in the past few years.