Async/await was introduced in NodeJS 7.6 and is currently supported in all modern browsers. I believe it has been the single greatest addition to JS since 2017. If you are not convinced, here are a bunch of reasons with examples why you should adopt it immediately and never look back.
Async/Await 101
For those who have never heard of this topic before, here’s a quick intro
- Async/await is a new way to write asynchronous code. Previous alternatives for asynchronous code are callbacks and promises.
- Async/await is actually just syntax sugar built on top of promises. It cannot be used with plain callbacks or node callbacks.
- Async/await is, like promises, non-blocking.
- Async/await makes asynchronous code look and behave a little more like synchronous code. This is where all its power lies.
Syntax
Assuming a function getJSON
that returns a promise, and that promise resolves with some JSON object. We just want to call it and log that JSON, then return "done"
.
This is how you would implement it using promises
And this is how it looks with async/await
There are a few differences here
Our function has the keyword
async
before it. Theawait
keyword can only be used inside functions defined withasync
. Anyasync
function returns a promise implicitly, and the resolve value of the promise will be whatever youreturn
from the function (which is the string"done"
in our case).-
The above point implies that we can’t use
await
at the top level of our code since that is not inside anasync
function.
await getJSON()
means that theconsole.log
call will wait untilgetJSON()
promise resolves and print its value.
Why Is It better?
Concise and clean
Look at how much code we didn’t write! Even in the contrived example above, it’s clear we saved a decent amount of code. We didn’t have to write.then
, create an anonymous function to handle the response, or give a namedata
to a variable that we don’t need to use. We also avoided nesting our code. These small advantages add up quickly, which will become more obvious in the following code examples.-
Error handling
Async/await makes it finally possible to handle both synchronous and asynchronous errors with the same construct, good oldtry/catch
. In the example below with promises, thetry/catch
will not handle ifJSON.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 thanconsole.log
in your production-ready code.Now look at the same code with async/await. The
catch
block now will handle parsing errors. -
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.
Just looking at this gives you a headache. It’s easy to get lost in all that nesting (6 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.
-
Intermediate values
You have probably found yourself in a situation where you call apromise1
and then use what it returns to callpromise2
, then use the results of both promises to call apromise3
. Your code most likely looked like thisIf
promise3
didn’t requirevalue1
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 1 & 2 in aPromise.all
and avoid deeper nesting, like thisThis approach sacrifices semantics for the sake of readability. There is no reason for
value1
&value2
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. -
Error stacks
Imagine a piece of code that calls multiple promises in a chain, and somewhere down the chain, an error is thrown.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
callAPromise
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 errorThis is not a huge plus when you’re developing on your local environment and have the file open in an editor, but it’s quite useful when you’re trying to make sense of error logs coming from your production server. In such cases, knowing the error happened in
makeRequest
is better than knowing that the error came from athen
after athen
after athen
… -
Debugging
A killer advantage when using async/await is that it’s much easier to debug. Debugging promises has always been such a pain for 2 reasons-
You can’t set breakpoints in arrow functions that return expressions (no body).
-
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.
-
-
You can
await
anything
Last but not least,await
can be used for both synchronous and asynchronous expressions. For example, you can writeawait 5
, which is equivalent toPromise.resolve(5)
. This might not seem very useful at first, but it's actually a great advantage when writing a library or a utility function where you don't know whether the input will be sync or async.Imagine you want to record the time taken to execute some API calls in your application, and you decide to create a generic function for this purpose. Here's how it would look with promises
You know that all API calls are going to return promises, but what happens if you use the same function to record the time taken in a synchronous function? It will throw an error because the sync function does not return a promise. The usual way to avoid this is wrapping
makeRequest()
inPromise.resolve()
If you use async/await, you won't have to worry about these cases because await allows you to work safely with any value, promise or not.
In Conclusion
Async/await is one of the most revolutionary features that have been added to JavaScript in the past few years. It makes you realize what a syntactical mess promises are, and provides an intuitive replacement.
Concerns
Some valid skepticism you might have about using async/await is that it makes asynchronous code less obvious: Our eyes learned to spot asynchronous code whenever we see a callback or a .then
, it will take a few weeks for your eyes to adjust to the new signs, but C# had this feature for years and people who are familiar with it know it’s worth this minor, temporary inconvenience.
Follow me on twitter @imgaafar
This article was originally published on Hackernoon
Top comments (27)
Important to mention that, although it looks cleaner, easier to read and everything, mainly for new developers, using
async/await
may trick you into not seeing points of opportunity for parallelism and you end up with a series of asynchronous call being treated as a plain synchronous code. For that cases where's there's in no need wait for things to be ran sequentially,Promise.all
to the rescue!I don't really agree with this. You can make the same mistake with promises or async/await. You can still chain the promises instead of calling them in parallel
You surely can. My point here is not to confuse new devs making them think one replaces the other, because it doesn't. They are two tools you can combine to make the best out of your async flows.
Good post, by the way!
tbh I tend to get a little weird with parallel promises and combine destructuring,
await
, andPromise.all()
:kinda gross, but personally I prefer it to returning to promise chains
Yeah, I do that myself (I don't think that's gross at all). That's the combination I think makes sense: we get the benefits of
async/await
without giving up the parallelism.Debugging async/await is actually a total pain if you're working in a project that's using babel. If you're stepping through your code, the minute you hit an async call, you're thrown into polyfill. If you're aware of a workaround, I'd love to know about it!
Disable that in Babel and let it spit out the actual async/await code instead of polyfilling it. (I do that for various other features as well, HTML custom elements being a big one, since there isn't a good way to write those in an ES3 compatible manner; new.target and Reflect didn't become available until after classes, and since you need to call 'super()' or equivalent in the constructor...)
Combined with webpack or the like, I'm not sure there, I have yet to use it.
Not all browsers that I need to support can use async/await, unfortunately, so I need to let babel handle that still.
I get that entirely, but for the initial testing and debugging of such code, I'd still turn it off. It lets you get your logic in order without having to debug a polyfill at the same time.
Ah, I gotcha - just for the purpose of debugging. Makes sense!
Hi, this is probably a silly question, but how do you disable those generators in Babel? Thanks
That's interesting I don't think I had this problem. Do you have source maps enabled?
For context, this is in a Vue project (created with vue-cli 3), and it's in a local dev environment so source maps shouldn't matter I don't think? I'll have to do another test with this and see if it's still an issue.
Works fine in chrome dev tools.
I like a lot about await, but I don't like having to use try/catch - it's not a construct I'm used to...it forces me to use 'let' instead of 'const' if I want to have access outside the catch block. Also, it indents one more level. Perhaps I need to learn try/catch more, but I note your examples tend not to show error handling, except the one where it is claimed as an advantage.
I share your dislike of try/catch, but it's the only way to handle errors in synchronous code, so we don't really have a choice. Async/await enables us to use just 1 approach to handle both sync & async errors. I'd prefer to handle all errors in the same way, even if it's not my favorite way 🤷♀️
I also don't really like the aspect that it hides what is going on. It looks synchronous, but definitely isn't.
Then there's the kangaroo behaviour when stepping through it, which isn't expected from its "sequential" appearance.
Imo, it's trying to be "too clever".
... having said that, I still find myself using it.
Async/Await are promises! What your examples do not show is that your async/await functions actually do not return just string, but instead they return Promise, you can get away by returning just a string, because the compiler or interpreter wrapps them in a Promise, actually the whole async/await function gets converted to an promise based one.
I have been working on JS/TS past 2 years. Understanding Asynchronous JS, had become a challenge. This article has really given a good clarity on Async/Await.
Kudos! to you man.
I got your reference by Kait Hoehne, she has written an excellent article titled - Is JavaScript Synchronous or Asynchronous?:
betterprogramming.pub/is-javascrip...
I will be referencing your article, if I plan to write a blog on JS synchrounous & Asynchronous.
I wouldn't call it better because this code does 2 things that I try to avoid generally:
I try to make code easier to read and trace, even if it becomes a little more verbose
Good read. Didn't know about #7, thanks man!
I'm not entirely sure if this is still the case, but there are also performance reasons at stake related to stacktrace reconstruction to justify preference of async/await vs promises in V8.
Source: mathiasbynens.be/notes/async-stack...
Great read, thanks!
Just a small note - you're missing the word "Reasons" in the title :)
Oops. Fixed it. Thanks
Great post 😄