Image for post
Image for post
I’m pretty sure these gears are all synchronous.

Fully Understanding Javascript Promises, Await, and Async

This essay is for beginners, and for anyone who from time to time gets confused about wtf javascript is doing when it comes to asynchronous flow.

For this example, we are going to load my user data from github’s api. We are going to pretend it is in an application that is doing a whole bunch of other stuff. Perhaps it is a webserver, and while it is waiting for github, it might enjoy serving other web requests.

So, here is the standard, old school way to do this. The callback!

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;function getUserDataWithCallback(cb) {
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
cb(xhr.responseText)
}
}
xhr.open('get', 'https://api.github.com/users/bluejack', true)
xhr.send()
}
getUserDataWithCallback(function(text) {
console.log(text)
})

Great. That works. Clean, clear, obvious.

But just to make sure everyone’s with me, what happens here is this:

(1) flow returns the XMLHttpRequest object from the require statement.

(2) flow next calls getUserDataWithCallback which passes an anonymous function in as the first parameter to that function.

(3) flow creates an object to handle this specific request: xhr — and we immediately register another function on this object with xhr.onreadystatechange: our handler for what to do when the request is completed.

(4) flow opens the connection and sends the request.

(5) flow returns to the calling function, and leaves the script entirely.

(6) I’m running this in node. The process doesn’t terminate, because we’re still holding the network resource.

(7) Eventually the network resource returns, and the state of the xhr object becomes the mysterious magical 4, and XMLHttpRequest calls the function we registered for this event. We now have text we can use.

(8) flow calls the callback function with that text

(9) the callback function writes the text to the console.

I went through this flow in excruciating detail, because from what I’ve seen on stack overflow, many beginners do not fully grasp the flow even of callbacks, and if you don’t grasp that, you will certainly not grasp what follows.

We’ll keep things a little tighter from here on out.

The important point to realize is that after flow exited the end of the script, the single execution thread would be available to do other work, were this in a larger application. The webserver, serving pages. Only when the network request was complete and the state changed, would our code “return to life.”

So, a callback is a straightforward manner of handling asynchronous events. It seems quite clean… until you start getting into the weeds of asynchronous events based on other asynchronous events. Suddenly we start to get into what many people call ‘callback hell’ — dozens of nested callbacks with various branches to handle error cases somewhere along the way. It’s horrible.

The next tool in the toolbox is the Promise.

Promises promise to make life better.

Here’s a Promise based implementation, in it’s standard form:

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;function getUserDataWithPromise() {
var xhr = new XMLHttpRequest();
return new Promise(function(resolve, reject) {
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status >= 300) {
reject("Error, status code = " + xhr.status)
} else {
resolve(xhr.responseText);
}
}
}
xhr.open('get', 'https://api.github.com/users/bluejack', true)
xhr.send();
});
}
getUserDataWithPromise().then(function(result) {
console.log(result)
}, function(error) {
console.log(error)
})

If you look at it, a promise is really just a fancy way of packing up a callback.

The flow is exactly the same as with the callback, but instead of returning nothing (in the case of the callback), it return a Promise object, which was created with one function… the body of our work.

The Promise executes that function, and passes it two functions of its own: resolve and reject. When our function calls resolve or reject it has the same effect as calling the callback, as you will shortly see.

This example uses standard short syntax. getUserDataWithPromise returns a Promise object, and we immediately call then on that object, and we pass it two functions. The first is called when our work-code calls resolve, while the second function is called should our work-code call reject.

Although this may seem more confusing than the simple callback, the glory of it is that it is a standard way of handling error cases, and Promises can cleanly and easily chained with consolidated error handling. Callback hell is no more.

To make this a little more explicit, you could also write our outermost code like so:

var userDataPromise = getUserDataWithPromise()userDataPromise.then(function(result) {
console.log(result)
}, function(error) {
console.log(error)
})

All clear, right?

But a promise just isn’t good enough for some people, it turns out, so we also have await and async. The problem with Promises is that they can be cumbersome and heavyweight and a bit confusing as to where flow control has ended up when all we want to do is wait for something to happen outside our our single Javascript process. Promises clean up error handling… but only to a certain degree.

Let’s await no longer.

Since you understand Promises, let’s begin with async — this is a keyword used to declare a function. It guarantees that this function will return a promise, and indicates that this function could have some asynchronous behavior inside. What’s more, you don’t need to write the promise code!

async function getUserData() {
return "Bluejack"
}

This function does not, contrary to appearances, return the string “Bluejack” — it returns a Promise that will resolve to the string “Bluejack”:

Watch:

$vim blue.js:async function getUserData() {
return "Bluejack"
}
console.log(getUserData())$ node blue.js:
Promise { 'Bluejack' }

Instead, edit our call to correctly use a Promise:

getUserData().then(function (val) { console.log(val) })$ node blue.js
Bluejack

I tend to prefer my code to be painfully clear, so I don’t really like allowing Javascript to make promises on my behalf. But there is another reason for the async keyword, and that is so that we can use the await keyword. When we use await it must be in a function declared async.

await is an alternative to then in working with promises, and it results in code that looks synchronous, but does not block our process. Because it can only be used in a function marked async it cannot be used in the top level of code.

Here is an update on our example.

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;async function getUserDataWithPromise() {
var xhr = new XMLHttpRequest();
return new Promise(function(resolve, reject) {
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status >= 300) {
reject("Error, status code = " + xhr.status)
} else {
resolve(xhr.responseText);
}
}
}
xhr.open('get', 'https://api.github.com/users/bluejack', true)
xhr.send();
});
}
async function logUserData() {
try {
let user = await getUserDataWithPromise()
console.log(user)
} catch (err) {
console.log(err)
}
}
logUserData()

So: we still need getUserDataWithPromise() because XMLHttpRequest.onreadystatechange is the “wake up call” that triggers all downstream flow, and we can’t wait on that with any of our syntactic asyncing and awaiting. These days most network functionality features and libraries will use promises, so this would be beautifully clean using fetch or axios.

But, we can now encapsulate that legacy code with clean, modern javascript. Using await we write an asynchronous function that looks synchronous: logUserData().

One of the beauties of async / await is that code can look very clean. Also, using try/catch blocks to handle the reject function of one or more nested rejected promises can be both elegant and far more stable (when you remember to use them).

But this comes with a few gotchas that seem to regularly trip up developers.

First, unless promises are well understood, await/async can lure us into a false sense of synchronous behavior. Remember that in our example, logUserData() returns immediately with a promise simply because it is declared async. In this case, it is a promise that won’t offer us any data, since we are not returning anything. We could make the mistake of thinking that we call logUserData() and only get a result after our await completes. No, no. Not so.

Secondly, the anonymous creation of Promises resulting from the async keyword is a funny little bit of magic that seems to cause no end of confusion. Remember that the promise resolves to any result returned by the function, including undefined if you don’t have an explicit return.

The true key to understanding async and await is that they are syntactical features of the javascript language designed to handle promises within a try/catch model of error handling, but the low-level understanding that a promise is just a fancy way of encapsulating a callback, and the high-level understanding that async functions always return a promise immediately.

Good luck out there!

Image for post
Image for post
Remember these days? Good old cube farms! I tried to liven mine up with plants, but the fluorescent light killed them anyway.

Written by

Building web applications since 1992. Crikey, that’s a long time.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store