ES2017 was finalized in June, and with it came wide support for my new favorite JavaScript feature: async
functions! If you’ve ever struggled with reasoning about asynchronous JavaScript, this is for you. If you haven’t, then, well, you’re probably a super-genius.
Async functions more or less let you write sequenced JavaScript code, without wrapping all your logic in callbacks, generators, or promises. Consider this:
function logger() {
let data = fetch('http://sampleapi.com/posts')
console.log(data)
}
logger()
This code doesn’t do what you expect. If you’ve built anything in JS, you probably know why.
But this code does do what you’d expect.
async function logger() {
let data = await fetch('http://sampleapi.com/posts')
console.log(data)
}
logger()
That intuitive (and pretty) code works, and its only two additional words!
Async JavaScript before ES6
Before we dive into async
and await
, it’s important that you understand promises. And to appreciate promises, we need go back one more step to just plain ol’ callbacks.
Promises were introduced in ES6, and made great improvements to writing asynchronous code in JavaScript. No more “callback hell”, as it is sometimes affectionately referred to.
A callback is a function that can be passed into a function and called within that function as a response to any event. It’s fundamental to JS.
function readFile('file.txt', (data) => {
// This is inside the callback function
console.log(data)
}
That function is simply logging the data from a file, which isn’t possible until the file is finished being read. It seems simple, but what if you wanted to read and log five different files in sequence?
Before promises, in order to execute sequential tasks, you would need to nest callbacks, like so:
// This is officially callback hell
function combineFiles(file1, file2, file3, printFileCallBack) {
let newFileText = ''
readFile(string1, (text) => {
newFileText += text
readFile(string2, (text) => {
newFileText += text
readFile(string3, (text) => {
newFileText += text
printFileCallBack(newFileText)
}
}
}
}
It hard to reason about and difficult to follow. This doesn’t even include error handling for the entirely possible scenario that one of the files doesn’t exist.
I Promise it gets better (get it?!)
This is where a Promise
can help. A Promise is a way to reason about data that doesn’t yet exist, but you know it will. Kyle Simpson, author of You Don’t Know JS series, is well known for giving async JavaScript talks. His explanation of promises from this talk is spot on: It’s like ordering food a fast-food restaurant.
- Order your food.
- Pay for your food and receive a ticket with an order number.
- Wait for your food.
- When your food is ready, they call your ticket number.
- Receive the food.
As he points out, you may not be able to eat your food while you’re waiting for it, but you can think about it, and you can prepare for it. You can proceed with your day knowing that food is going to come, even if you don’t have it yet, because the food has been “promised” to you. That’s all a Promise is. An object that represents data that will eventually exist.
readFile(file1)
.then((file1-data) => { /* do something */ })
.then((previous-promise-data) => { /* do the next thing */ })
.catch( /* handle errors */ )
That’s the promise
syntax. Its main benefit is that it allows an intuitive way to chain together sequential events. This basic example is alright, but you can see that we’re still using callbacks. Promises are just thin wrappers on callbacks that make it a bit more intuitive.
The (new) Best Way: Async / Await
A couple years ago, async functions made their way into the JavaScript ecosystem. As of last month, its an official feature of the language and widely supported.
The async
and await
keywords are a thin wrapper built on promises and generators. Essentially, it allows us to “pause” our function anywhere we want, using the await
keyword.
async function logger() {
// pause until fetch returns
let data = await fetch('http://sampleapi.com/posts')
console.log(data)
}
This code runs and does what you’d want. It logs the data from the API call. If your brain didn’t just explode, I don’t know how to please you.
The benefit to this is that it’s intuitive. You write code the way your brain thinks about it, telling the script to pause where it needs to.
The other advantages are that you can use try
and catch
in a way that we couldn’t with promises:
async function logger () {
try {
let user_id = await fetch('/api/users/username')
let posts = await fetch('/api/`${user_id}`')
let object = JSON.parse(user.posts.toString())
console.log(posts)
} catch (error) {
console.error('Error:', error)
}
}
This is a contrived example, but it proves a point: catch
will catch the error that occurs in any step during the process. There are at least 3 places that the try
block could fail, making this by far the cleanest way to handle errors in async code.
We can also use async functions with loops and conditionals without much of a headache:
async function count() {
let counter = 1
for (let i = 0; i < 100; i++) {
counter += 1
console.log(counter)
await sleep(1000)
}
}
This is a silly example, but that will run how you’d expect and it’s easy to read. If you run this in the console, you’ll see that the code will pause on the sleep call, and the next loop iteration won’t start for one second.
The Nitty Gritty
Now that you’re convinced of the beauty of async and await, lets dive into the details:
async
andawait
are built on promises. A function that usesasync
will always itself return a promise. This is important to keep in mind, and probably the biggest “gotcha” you’ll run into.- When we
await
, it pauses the function, not the entire code. async
andawait
are non-blocking.- You can still use
Promise
helpers such asPromise.all()
. Here’s our earlier example:async function logPosts () { try { let user_id = await fetch('/api/users/username') let post_ids = await fetch('/api/posts/<code>${user_id}') let promises = post_ids.map(post_id => { return fetch('/api/posts/${post_id}') } let posts = await Promise.all(promises) console.log(posts) } catch (error) { console.error('Error:', error) } }
- Await can only be used in functions that have been declared Async.
- Therefore, you can’t use
await
in the global scope.// throws an error function logger (callBack) { console.log(await callBack) } // works! async function logger () { console.log(await callBack) }
Available now!
The async
and await
keywords are available in almost every browser as of June 2017. Even better, to ensure your code works everywhere, use Babel to preprocess your JavaScript into and older syntax that older browsers do support.
If you’re interested in more of what ES2017 has to offer, you can see a full list of ES2017 features here.
JavaScript syntax is going to be as same as C#! that’s cool! specially for .NET developers :)
Look at code listings above: replace
let
withvar
(which indeed is a valid JS and C# keyword) andfunction
withTask
(orTask<ReturnType>
). That IS C# code!What a gem.
Doesn’t await just make asynchronous calls synchronous?
Nope, it doesn’t. And that’s exactly the reason why you have to use
await
exclusively within async functions.Sort of, but it does it in a non blocking way. The advantage of this is that other asynchronous functions can still run while we are waiting. For example, if you were waiting on a sleep function in one function’s stack, an event could still fire from a different section of code.
The following code should print “main start”, then “interrupting the middle of main”, and then finally “main end”.
So in essence, async/await is a way to make a single task synchronous without blocking javascript’s event loop;
That’s the gist of it.
The only caveat is that its a Promise wrapper, so you have to be expecting a Promise return. 90% of the time its a non-issue, but it can cause trouble if you aren’t expecting it.
No. It looks like it’s asynchronous.
await
basically introduces syntactic sugar. For example:can be “transpiled” to:
In short, an
async
function always returns aPromise
, andawait
is syntax for getting the resolved value (and the usualtry...catch
for the rejected one).I guess that the sleep function will look like this.
It could be simplified to:
Edit: missed a paren
Nice article, but I don“t like the missing semicolon :D
Nice article – maybe stress out the the part of handling rejected promises
this is a game changer :0
No, I’m not
I have this idea that you if think about asynchronous tasks all as ordering something from Amazon: you buy a thing, but then you don’t stop right there, you can do other stuff in the meanwhile.
It’s worth mentioning that top-level
await
isn’t available. What I mean is that, at the moment, you can’t open the console and typeawait fetch("https/css-tricks.com")
, you have to wrap it in anasync
IIFE.There’s a discussion about allowing it but there’s a plethora of problems behind.
There is a top-level
await
in the latest Dev builds of Chrome: https://developers.google.com/web/updates/2017/08/devtools-release-notes#awaitDoes that mean i can do this:
logPosts().then( results => mylog(results))
E
What you’re describing is the promise API which, as stated on the post, had been available for a while now.