From callbacks to async/await: A History of Asynchronous Javascript

Dan Lemon / Oct 19, 2017

Web components are a collection of web standards allowing you to create new HTML tags with custom names, reusability and full encapsulation on styles & markup. Because they are included in the HTML spec, they are compatible with the big frameworks. None of the big frameworks can avoid supporting HTML (as this is required to render in a browser) therefore support for web components is guaranteed. 

Synchronous Functions

In Javascript, most functions get called one at a time, or synchronously. Execution happens in order, one a time. Standard functions like Math.random, Math.floor are synchronous functions. They get executed immediately, and because Javascript is single-threaded (in-effect, if not necessarily in reality), the functions get called to the exclusion of any other runtime operation. This is the default behavior of JavaScript functions.

Scenario: You are in a queue to get a movie ticket. You cannot get one until everybody in front of you gets one, and the same applies to the people queued behind you.

So synchronousity is about dependency. Line 2 waits for Line 1 because Line 2 is dependent on line 1. This code is said to have a synchronous flow. Because these lines depend on one another, the user waits until Math.random() has returned a value. This happens so fast that it doesn't hurt UX, but it does take a very small amount of time.

Asynchronous Functions

Some functions like setTimeout, and setInterval take an unpredictable amount of time to finish executing. Because the runtime allows these functions to disrupt the one-at-time flow of our application, they are called asynchronous functions. Their callbacks are permitted to enter the stack at any time, and we don't want the program to freeze while the user waits around for that.

Scenario: You are in a restaurant with many other people. You order your food. Other people can also order their food, they don't have to wait for your food to be cooked and served to you before they can order. In the kitchen, restaurant workers are continuously cooking, serving, and taking orders. People will get their food as soon as it is cooked.

There's a big gotcha: getting your order in before your neighboring table doesn't guarantee that your food will come out before theirs. In the following example, the callback we pass to setTimeout is the order we give to the waiter, and the value 'x' is the food we're waiting for.

When the runtime goes to execute setTimeout, it's callback, reassignX is stored in-memory, and then the browser basically issues itself a command such as, "wait 1 sec, then give me reassignX back". SetTimeout is immediately removed from the stack, just as if it were any other function that had returned. Execution then continues as usual. After 1 second, the browser automatically pushes a reference to reassignX onto the bottom of the message queue where it will soon wind up on the call stack.

Common examples are functions that retrieve remote data, such as fetch.

^ this is async working as intended. We don’t want our app to grind to a halt every time we call one of these functions. If fetch was a synchronous function, the app would stop while the user waited for data to come back. Nothing can load, no spinning animation would be possible, no buttons would be clickable --async to the rescue.

An async function is the bad friend you invite to your party: you can’t rely them to show up on time, so you start the party without them. The user might find themselves forced to wait if we tried to treat an async function like setTimeout the same way we our dependable friend, Math.random(). Much like the guests at our dance party have to wait if we relied on our bad friend to bring the DJ equipment.

The Problem

Let’s look more closely at the problems of async. Quick quiz: how long will this code take to run from start to finish?

Time’s up! The answer is…. 3 seconds.

Remember, we're in async land, so when an async function gets called, it's basically getting skipped, and the runtime moves on.

Right now, all these timer functions start at the same time. This is probably not the behavior we want.

What we really want is to create an interval timer, like in a relay. The timer for the second runner won’t start until the first runner completes their lap. Let’s look at a couple ways to do just that.

Solution One - Nested Callbacks

setTimeout takes a callback function that gets executed when a set time has elapsed. Think of this as a Russian nesting doll of callbacks. This function might look like this:

BUT, don’t forget that the first parameter to setTimeout is the function callback. This gets executed after some amount of time. So in order to execute timer2, that callback itself needs to be a setTimeout function. Which--of course--accepts a callback, which in turn needs to be passed to our last setTimeout function, which we pass as a callback…

(╯°□°)╯︵ ┻━┻)

Callbacks seem simple on the surface but quickly become messy, hard to read, and hard to maintain. Just typing that example was painful.

What we really want is something that reads like this:

  1. timer1 runs to completion.
  2. timer2 runs to completion.
  3. timer3 runs to completion.

This leads us to the next solution.

Solution Two - Promises

A Promise is a proxy for a value. It's basically an object with a .then method that takes a callback function. That callback fires when the Promise's resolve() fires. Using a wrapper like the one above, any async function that uses callbacks can easily be made to use Promises.

We can promise-ify our setTimeout call like this:

Chaining .then allows us to read down the page, viewing our callbacks in a friendlier way. As you can see, we get closer to approaching our goal of code that reads like this:

  1. timer1 runs to completion.
  2. timer2 runs to completion.
  3. timer3 runs to completion.

So, Promise.then is pretty great, certainly better than nested callbacks. BUT we now are forced to pollute our code with a .then every time we call an async function. Things can get a little messy, like in this example:

Wouldn't it be nice if we could get rid of all these individual .then calls, and just tell Javascript: “hey, this whole code block is going to be asynchronous”, and then everything just runs in order?

Something that might look like this?

We can!

Solution Three - Generator Functions

We can use ES6 generators to say goodbye to .then and create an “async wrapper” for our Promises.

A generator is a pause-able function.

Yield expressions are tricky because they have both an output and an input: on the first .next() they output a value, on the second, they get replaced by a value.

Doesn’t that sound handy? Remember when we wanted to write something like this?

With generators, we can actually write our 'asyncify' function:

If you run the above command, you can see it really works!

Unfortunately, asyncify is not for production, as it offers no error handling, but it's a considerable upgrade from the old way of structuring Promises with .then. Furthermore, yield and function* are not great keywords for what we’re doing. But there is, you guessed it, there’s a better way.

Solution Four - Async/await

If that ‘asycify’ generator wrapper we just made looked familiar, there’s a reason for it: it’s basically an ES2017 async function! In fact, async functions really are generators when you look under the hood! We can replace our nifty asyncify function with a native Javascript implementation. Instead of:

we write:

BOOM.

As of writing this, async/await is supported in the latest version of all major browsers (other than IE), and NodeJS 7.6+. It’s maybe not worth refactoring all your existing Promise code, but it’s definitely worth using as the defacto technique for any new projects.

Special thanks to Amazee's Felix Morgan and George Mauer (who's also an author of a cool generator-based functional library) for help with this blog post.