Truly understanding Async/Await
In this article, I’ll attempt demystifying async/await
by learning what they really are and what’s actually going on behind the scenes.
You know what it does, but do you know how it does it?
Most developers have a love-hate relationship with JavaScript. One reason for this is that it falls victim to one of its best qualities: easy to learn, hard to master.
One of the ways in which this quality is noticeable is how many developers tend to assume the language works in a certain way, but in actuality, something very different is going on behind the scenes.
This difference manifests in the details, and causes frustration.
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.
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 creating object instances, and a prototype IS an object 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 Functions are an addition to the language already included in ECMAScript’s latest draft (Stage 4). You can use them today using the Babel transpiler.
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
Promises are a very clever nice way to deal with asynchronous code.
A Promise is an object that represents an asynchronous task that will eventually finish. They look like this when used:
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 finishedcatch
: 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 of 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 that allow them to be chained. Let's 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.
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) {
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"
});
Okay, we are almost there, I promise. (pun unintended).
Async functions are functions that return promises
That’s right. This is the reason I took the time to briefly explain Promises, because to really understand async/await
, you need to know how Promises work, kind of like to really understand classes in JavaScript, you need to understand Prototype.
How it works
- There are Async Functions. These are declared by prepending the word
async
in their declaration:async function doAsyncStuff() { ...code }
. - 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 use 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 is 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.log(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
So, to finish I’d like to show a couple of examples of how async/await
roughly translates into Promises. I hope that this will help you see how async functions are simply syntactic sugar for creating functions that return and wait for 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;
}
(async function () {
try {
var result = await foo();
console.log(result); // Logs 5
}
catch(err) {
console.log(err);
}
})();
// == 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);
});
);
}
new Promise(function() {
foo().then(function(result) {
console.log(result); // Logs 5
}).catch(function(err) {
throw err;
});
});
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.
Wrapping Up
I hope this clears up the picture a little bit. async/await
is easy, as long as you understand Promises well.
Feel free to comment or suggest corrections/additions. If you have other or better examples for the end of this post, share them in the comments and I will gladly include them.