Truly understanding Async/Await

In this article, I’ll attempt to demystify the async/await syntax by diving into what it really is and how it really works behind the scenes.

You know what it does, but do you know how it does it?

For example, I have no doubt that one of latest changes in the standard has caused many of us to develop misconceptions about behavior: classes. Javascript does NOT have classes, in reality, Javascript uses Prototypes, singleton objects from which other objects inherit from. In fact, all objects in Javascript have a prototype from which they inherit. This means that Javascript’s “classes” do not behave exactly like classes. A class is a blueprint for create object instances, a prototype IS an instance that other object instances delegate work to, a prototype is not a blueprint, it actually exists, it is there.
This is why you can actually add a new method to Array and suddenly all arrays can use it. This can be done in runtime, affecting an already instanced object.

var someArray = [1, 2, 3];Array.prototype.newMethod = function() {
console.log('I am a new method!');
};
someArray.newMethod(); // I am a new method!// The above code would not be possible with real classes, because // modifying a blueprint does not modify whatever was built with it.

In short, classes in Javascript are syntactic sugar for Prototype Inheritance.

My main point here is that, you have to learn how a language really works, beyond its syntax, if you want to fully understand its capabilities and limitations.

Async/Await Spec

async/await attempts to solve one of the biggest pains in the language since its beginning: asynchrony. If you do not understand the concept of asynchronous code, I suggest you to read about that first before you keep reading this article.

Over the years, we’ve had multiple ways to deal with asynchrony without going crazy. For most of Javascript’s life, we’ve relied on Callbacks:

setTimeout(function() {
console.log('This runs after 5 seconds');
}, 5000);
console.log('This runs first');

Callbacks are nice and all, but what if we have to do things sequentially?

doThingOne(function() {
doThingTwo(function() {
doThingThree(function() {
doThingFour(function() {
// Oh no
});
});
});
});

What you see above is sometimes referred to as Pyramid of Doom or Callback Hell and there are websites in their honor. Not good.

Behold: promises

function buyCoffee() {
return new Promise((resolve, reject) => {
asyncronouslyGetCoffee(function(coffee) {
resolve(coffee);
});
});
}

buyCoffee returns a Promise that represents the process of buying coffee. The resolve function signals the Promise instance that it has finished. It receives a value as an argument, which will be available through the promise later on.

A Promise instance has two main methods:

  • then : This runs a callback you pass to it when the promise has finished
  • catch : This runs a callback you pass to it when something went wrong, which caused the promise to reject instead of resolve. reject is either manually called (For example, we are doing an AJAX call and received a server error) or it is called automatically if an uncaught exception is thrown inside the Promise’s code. Important: promises that were rejected because of an exception will swallow the exception. This means that if you don’t have all your Promises correctly chained or if there’s no catch call in any promise of the chain you will find yourself in a different kind of hell where your code fails silently, this can be extremely frustrating so do avoid the situation at all costs.

Promises have some other very interesting properties, which allow them to be chained. Lets say we have other functions that return a Promise. We could do this:

buyCoffee()
.then(function() {
return drinkCoffee();
})
.then(function() {
return doWork();
})
.then(function() {
return getTired();
})
.then(function() {
return goToSleep();
})
.then(function() {
return wakeUp();
});

Using callbacks here would’ve been very bad for our code’s maintainability and maybe our sanity too.

If you are not used to Promises the above code might look counter-intuitive, this is because Promises that return a Promise in their then method will return a Promise that only resolves when the returned Promise resolves. And they will do it with the returned Promise’s return value (sigh, sorry I couldn’t word that better).

Example to the rescue!

const firstPromise = new Promise(function(resolve) {
return resolve("first");
});
const secondPromise = new Promise(function(resolve) {
resolve("second");
});
const doAllThings = firstPromise.then(function() {
return secondPromise;
});
doAllThings.then(function(result) {
console.log(result); // This logs: "second"
});

If you want to learn more about Promises, check this awesome github repo with everything about the topic

Okay, we are almost there, I promise. (pun unintended).

Async functions are functions that return promises

How it works

  • Your code can be paused waiting for an Async Function with await
  • await returns whatever the async function returns when it is done.
  • await can only be used inside an async function.
  • If an Async Function throws an exception, the exception will bubble up to the parent functions just like in normal Javascript, and can be caught with try/catch. But there’s a catch (again, pun unintended): Just like in Promises, exceptions will get swallowed if they are not caught somewhere in the chain. This means that you should always try/catch
    wherever a chain of Async Function calls begins. It is a good practice to always have one try/catch per chain, unless not doing this is absolutely necessary. This will provide one single place to deal with errors while doing async work and will force you to correctly chain your Async Function calls.

Let’s look at some code

// Some random async functions that deal with value
async function thingOne() { ... }
async function thingTwo(value) { ... }
async function thingThree(value) { ... }
async function doManyThings() {
var result = await thingOne();
var resultTwo = await thingTwo(result);
var finalResult = await thingThree(resultTwo);
return finalResult;
}
// Call doManyThings()

This is how code with async/await looks like, it very close to synchronous code, and synchronous code is much easier to understand.

So, since doManyThings() is an Asynchronous Function too, how do we await it?
We can’t. Not with our new syntax. We have three options:

  • Let the rest of our code execute and not wait for it to finish, which we might even want in many cases.
  • Call it inside another Async Function wrapped with a try/catch block.
  • or… Use it as a Promise.
// Option 1:
doManyThings();
// Option 2:
(async function() {
try {
await doManyThings();
} catch (err) {
console.error(err);
}
})();
// Option 3:
doManyThings().then((result) => {
// Do the things that need to wait for our function}).catch((err) => {
throw err;
});

Again, they functions that return promises

A simple async function:

// Async/Await version
async function helloAsync() {
return "hello";
}
// Promises version
function helloAsync() {
return new Promise(function (resolve) {
resolve("hello");
});
}

An async function that awaits another async function’s result

// == Async/Await version ==
async function multiply(a, b) {
return a * b;
}
async function foo() {
var result = await multiply(2, 5);
return result;
}
// Errors will be swallowed here(async function () {
var result = await foo();
console.log(result); // Logs 5
})();
// == Promises version ==
function multiply(a, b) {
return new Promise(function (resolve) {
resolve(a * b);
});
}
function foo() {
return new Promise(function(resolve) {
multiply(2, 5).then(function(result) {
resolve(result);
});
);
}
// Errors will be swallowed herenew Promise(function() {
foo().then(function(result) {
console.log(result); // Logs 5
});
});

Note that the above use of Promises is not recommended, I simply shaped them in a way that was easier to use for comparison against the async/await examples.

Example of why we care

async function foo() {
someArray.forEach(function (value) {
doSomethingAsync(value);
});
}

This is all good so far, we are doing doSomethingAsync multiple times in parallel because we are not await-ing. But how would we do it?

Not like this:

async function foo() {
someArray.forEach(function (value) {
await doSomethingAsync(value);
});
}

The above throw’s a syntax error because we are passing to forEach a synchronous function.

No problem right? We should just pass it an Async Function. Nope.

async function foo() {
someArray.forEach(async function (value) {
await doSomethingAsync(value);
});
}

What’s wrong here? Well, lets see what this could translate to. I’ll avoid being to verbose with promises here and translate this as we actually would if we for some reason had to:

function foo() {
someArray.forEach(function () {
// this is returning a promise
return doSomethingAsync(value);
});
}

The problem is that, forEach does not await for your Async Function or, thinking with Promises, it does not wait for one iteration’s returned promise to resolve before calling the next one.

Also in our examples above we should’ve been awaiting the forEach call too.

So how do we solve this? Well, sadly we simply cannot use forEach here. In fact, no synchronous iterators will work. We need iterators that know how to deal with promises.

There’s one that does work though. The modern version of a for loop, "for of” which does await for promises.

This works:

for (item of someArray) {
await foo();
}

If you cannot use “for of”, you can implement an iterator that supports promises, or use a library like bluebird’s Promise.each:

var Promise = require('bluebird');function foo() {
await Promise.each(someArray, async function(value) {
await doSomethingAsync(value);
});
}

Understand Promises and you understand async/await .

Wrapping Up

Founder of www.gambitstudio.com, a small, super efficient, no-bullshit software development agency.