ES2015 Promises & ES2017 Async/Await

Kerri Shotts
15 min readJul 14, 2017

--

I swear, it feels like it’s been forever since we’ve been using promises in JavaScript, but they actually only became part of the JavaScript language with the ES2015 standard. Up until that point there were various libraries that provided Promises, and there are also plenty of polyfills available for ES2015 promises as implemented in case you need to support an engine that doesn’t support ES2015’s promises.

Playground link: http://alpha.trycarbide.com/@kerrishotts/5123760d516a0acbc53cb1810ca6fc3f

Introduction to Promises

A promise is just that: a promise to either return a result at some point in the future or a promise to reject with an error at some point in the future. When a promise knows the return result, the promise resolves with the return result. When a promise fails because of an error, the promise rejects with the reason why the promise failed.

Promises were designed to make it easier to write sequential asynchronous code easier. It turns out that promises also help to avoid a common JavaScript pitfall: callback hell, which when nested, becomes The Pyramid of Doom. You’ve certainly seen callbacks in action, but if not, here’s a reminder:

setTimeout(delayedFunction, 500);

Here, delayedFunction is expected to be a function and it will be called back in half a second (500 milliseconds). With that in mind, let’s use setTimeout to create an artificially slow function that divides two numbers:

function slowDivCB(successCB, errorCB, a, b, ms = 500) { 
setTimeout(function() {
if (b === 0) errorCB(new Error("Can't divide by zero"));
else successCB(a / b);
}, ms);
}

Already you can see that this function is going to be a joy to invoke. A simple division looks like this:

slowDivCB(function (r) {console.log(r);}, 
function (err) {console.err(err);}, 10, 2);

After a brief pause, we get our answer logged to the console: “5”. It works!

What if, however, we wanted to evaluate something more complex, like (((900 / 3) / 5) / 2)? Each step depends upon the results of the previous division. Well, that would look something like this:

slowDivCB(function (c) {
slowDivCB(function (d) {
slowDivCB(function (e) {
console.log(e);
}, function (err) {console.error(err);}, d, 2)
}, function (err) {console.error(err);}, c, 5)
}, function (err) {console.error(err);}, 900, 3);

Oh, dear. Now you know what I mean by The Pyramid of Doom. You can see how things start to get hard to read in a hurry. This could be flattened out, of course, by promoting the anonymous functions to named functions, but even then, it’s not always easy to follow the path of invocations.

Promises hoped to avoid these issues, and they have succeeded… to a degree. It’s still possible to end up in a Pyramid of Doom or end up in a different sort of hell — that of long chains, but when used correctly, promises have proven immensely useful. Plus, the API makes reading code that uses promises easier than reading code that uses lots of nested callbacks.

So what would the above look like when written as a promise? Before we do that, let’s look at how we construct a promise with ES2015.

If you’re using an API that returns promises, you shouldn’t have to write code like what’s below; ideally you should only need to create new promises like this when you need to wrap code that doesn’t already return promises.

let aPromise = new Promise((resolve, reject) => { /* code */ });

The resolve and reject parameters are important (and are also why we used the terms earlier). Each is a function provided to our promise callback by the promise engine itself, and can take a value. The code inside the promise callback can call resolve(data) with a result when the it is known, or it can call reject(err) with an error should something prevent the promise from resolving.

Note: You should treat rejections like exceptions — only reject in dire circumstances.

We can rewrite our slow divide function like this:

function slowDiv(a, b, ms = 500) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (b === 0) {
reject(new Error("Can't divide by zero"));
} else {
resolve(a / b);
}
}, ms);
});
}

Ok, so it looks pretty similar to the version that didn’t use a promise. The real difference is that the function signature is a bit nicer — we don’t have to pass in success or failure callbacks, because the returned promise will take care of that for us.

How, do you ask? After the division finishes (or fails), the call to resolve or reject has to go somewhere, right?

Let’s see how we’d write chained division, and that should make things a lot clearer:

slowDiv(900, 3)
.then(c => slowDiv(c, 5))
.then(d => slowDiv(d, 2))
.then(e => console.log(e))
.catch(err => console.error(err));

Promises rearrange the pyramid of callbacks into a chain of callbacks instead, which more realistically illustrates what’s going on. In each case, the result of our division depends on the previous step, so although our division is proceeding asynchronously, the calculation is still proceeding in sequence. It’s really hard to tell that from the pyramid of doom, but here it’s obvious — each then is called in sequence when the previous result resolves. Should a promise reject, the next catch down the line is called instead.

Technically then can take both a success and a failure function, in which case any rejection will call the failure function instead, but I don’t use this pattern often.

So far so good, right? There are a few issues, however:

  • Non-JavaScript error handling — that is, we’re not able to use try...catch — instead we catch with a catch handler
  • Everything in the chain has to be wrapped with then or catch
  • Easily abused by having long anonymous functions that make control flow difficult to follow

We’ll address these shortcomings in a bit with a new ES2017 feature, but for now, let’s go over a few more things regarding promises.

Promise Pitfalls

There are several pitfalls you need to be aware of when using promises, and it’s important that you learn to avoid them.

It’s easy to end up in a situation where errors aren’t handled. This can often lead to those errors being “swallowed” — not reported. This makes debugging a pain in the rear. You should always seek to have a catch at the end of a promise chain in order to give your code a proper chance to deal with any errors that may have been generated.

Calling a function that returns a promise but failing to have a then or catch to respond to the result (or rejection) is not something that will be caught by the JavaScript engine. It might not even be an error at all (if that function causes side effects). But usually you want to do something with the result (or respond to an error), so not having a then or catch would usually be a bug. But since this isn’t technically an error, you’re going to have to be on guard for it yourself, since it’s perfectly valid JavaScript.

It’s important also to understand that promises begin executing immediately upon creation. They don’t wait for you to do something with them. If you need a promise to wait, you need to use something called a promise factory (a fancy term for a function that returns a promise).

Advanced Promises

There are several functions we can use to create promises and work with promises. Let’s go over those now.

First, we can create a promise that resolves instantaneously by calling Promise.resolve(value). We can also create a promise that rejects instantaneously by calling Promise.reject(error). But why would we ever do this?

Well, there are a couple of reasons. The first is simple: just because a promise can be a promise to return a value in the future, that doesn’t mean that promise must be delayed. If you know a value now, why not return that value now?

The second reason is a little more complex. If slowDiv threw an error before returning a promise, we’d actually have to handle that error outside the promise chain. None of our catch handlers would be triggered. On the other hand, if an error is thrown inside any of our handlers, the error will trigger the next catch in the chain. So now we’re mixing metaphors in a sense — we have to be on guard that the initial part of the promise chain could throw using try...catch, but every where else we’re using catch as part of a chain.

Eugh. Instead, being able to resolve immediately lets us write code that, if an error is thrown, will always trigger the next catch in the chain. Consider:

// less than ideal, since we have to have two error-handling
// mechanisms
function couldThrow(a, b) {
if (b === 0) throw new Error("b is zero!");
return new Promise(resolve => resolve(a / b));
}
try {
couldThrow(10, 0)
.then(r => console.log(r))
.catch(err => console.error(err));
} catch (err) {
console.error(err);
}

and compare to:

// much better! Only one error-handling mechanism required
function couldThrow(a, b) {
return Promise.resolve()
.then(() => {
if (b === 0) throw new Error("b is zero!");
return a / b;
});
}
couldThrow(10, 0)
.then(r => console.log(r))
.catch(err => console.log(err));

I rather like the latter better, if only so that I don’t have to reason about two different error handling mechanisms (and can also avoid having to repeat myself in the second error handler).

As an interesting aside, we can actually use Promise.resolve to rewrite our slow division function and avoid using new Promise entirely. I’ll leave that as an exercise to the reader, or you can just look at the accompanying playground. Of course, ultimately, a promise has to be created somewhere, so you’re not saving anything by doing this, but it is an interesting exercise.

We also often find ourselves needing to work with a number of promises at once. To help with that, we can use Promise.all and Promise.race.

Promise.all returns a promise that resolves when all the promises given to it have also resolved or rejects when any one promise rejects. Promise.race, on the other hand, resolves when any supplied promise resolves (hence the term “race”) and rejects when any promise rejects.

Imagine that we needed to calculate several slow divisions, but that the results didn’t depend on each other — we just want them to execute in parallel. With Promise.all we can write code like this:

Promise.all([
slowDiv(900, 3),
slowDiv(300, 100, 100),
slowDiv(5000, 10, 1000),
slowDiv(253, 11, 750)
]).then(results => console.log(results));

The result, after a few moments, will be [300, 3, 500, 23]. Technically, though, the promises resolved in the following order: 300/100, 900/3, 253/11, 5000/1000. The order passed to the then handler is based on the order in which the promises were created, not the order in which they were resolved.

Now, imagine that we had a set of divisions we needed, but we only cared about the one that resolved first. We could write that as follows:

Promise.race([
slowDiv(900, 3, 5000),
slowDiv(300, 100, 100),
slowDiv(5000, 10, 1000),
slowDiv(253, 11, 750)
]).then(result => console.log(result))
.catch(err => console.log(err));

… and the answer logged to the console will be “3”, since that promise is resolved in 100ms, well in advance of any of the other promises.

ES2017 Async/Await

Promises are incredibly useful, and let us write sequential asynchronous JavaScript that doesn’t block threads while waiting for a response, but it has come with a price: promises themselves don’t look a lot like normal JavaScript. It’d be nice to write code that looks synchronous and that would let us use normal language features like try...catch as well.

Enter ES2017’s async/await feature. It was originally intended to be in ES2015, but kept getting pushed back, and finally landed in ES2017. It lets us tag functions as asynchronous, and these functions will then return promises, eventually resolving to whatever value is returned (or rejecting with whatever error is thrown). We can then call these functions and await the resolution or failure of those promises. In this way we can write code that looks more synchronous than it really is.

We can declare that a function is asynchronous by adding an async keyword, like so:

async function foo() {
return 42;
}

Asynchronous functions will implicitly return promises that resolve to the function’s implicit or explicit return value. In this case, the return value is explicit, and so the returned promise will resolve to “42”. Had we not explicitly returned anything, the promise would resolve to “undefined”.

In case you’re wondering, arrow functions can be asynchronous as well:

const bar = async () => 42;

So far, these asynchronous functions resolve immediately, but that’s not terribly useful. We can use await to wait for other asynchronous functions or promises, like so:

async function asyncDiv(a, b, ms = 500) {
const result = await slowDiv(a, b, ms);
return result;
}

Remember our friend, slowDiv? You’ll remember that it returns a promise, which we can await here. When the promise resolves, it’ll be returned back to the caller.

An asynchronous function can have any number of await expressions, which makes it useful for indicating events that are asynchronous but that which execute in parallel. Remember our chained division example? Here’s that example written using async / await:

async function chainedDivision() {
const r1 = await asyncDiv(900, 3),
r2 = await asyncDiv(r1, 2),
r3 = await asyncDiv(r2, 5);
return r3;
}

At each await, execution pauses until the asynchronous function completes. It’s important to remember that during this time, the JavaScript engine and any other UI is not blocked, and so can remain responsive to other user input. (The only thing that is “blocked”, is whatever is relying on the results of this division.)

If we wanted to be super terse, we could have written the function like this:

async function chainedDivision() {
return asyncDiv(await asyncDiv(await asyncDiv(900, 3), 2),
5);
}

… but that’s not nearly as easy to read in my opinion!

Pssst! Where’d that first await go? Well, if our asynchronous function returns a promise, that’s fine! Anything awaiting us will just wait for the promise to resolve.

Inside asynchronous functions, we can also handle exceptions with normal try...catch constructs:

async function go() {
try {
const c = await asyncDiv(5, 0);
} catch (err) {
console.error(err.message);
throw err;
}
}

… which, to me, reads better than a chain of a lot of then and catch handlers.

Now, there’s something important you need to know about asynchronous functions: they poison the call chain. It’s not as simple as changing a function to be asynchronous and then expecting all the callers (all the way up the call stack) to be aware of the change — they have to await the result and be themselves async functions, or they have to treat the call to an asynchronous function like a promise.

Since asynchronous functions deal in promises, we can treat them as such. In fact, at the top level of our program, that’s all we can do, since it’s illegal to await a result outside of an async function. We can choose to discard the return (which will be a promise) if we want — the asynchronous function will still execute — or we can treat the return as a promise and use then and catch. For example:

go().catch(err => console.log(err.message)); 

My suggestion is to always have a catch at the end of a promise chain, and that includes calling async functions at the top level of a program. That way there’s no chance an exception will go unhandled.

To further show how promises and asynchronous functions can interact, let’s create an asynchronous function that waits until all the supplied promise factories resolve. It looks like this:

async function all(...promiseFns) {
return Promise.all(promiseFns.map(promiseFn =>
promiseFn()));
}

And we can call it like so (assuming we’re in an async function):

const results = await all(async () => await asyncDiv(900, 3), 
async () => await asyncDiv(300, 100, 100),
async () => await asyncDiv(5000, 10, 5000),
async () => await asyncDiv(253, 11, 750));

… but we actually can write the all method just like this:

function all(...promiseFns) {
return Promise.all(promiseFns.map(promiseFn => promiseFn()));
}

and it will work identically because await works happily with promises. So, don’t be surprised that when you’re using asynchronous functions and awaiting results, that you’re also using a lot of promise-related code as well, because ultimately, they are meant to be used together.

Promises, Async/Await, and Remote Resources

Promises map reasonably well to the concept of requesting resources over the network. Those resources aren’t typically available instantaneously — the request has to go out over the wire, a server has to process the request, and the response has to be sent back down the wire — and this can take tens or hundreds of milliseconds. As such, the request could return a promise that it will eventually resolve into a reference to the resource, and when it does so, it will let us know.

There is a catch, however: promises themselves aren’t cancellable. For example, we can’t abort a request whenever we want — we have to wait until it resolves or rejects on its own. And, because async/await deals in promises, any awaited function will have the same issue: the code will be blocked until either a resolution or a rejection. Therefore, depending on your needs, you may want to pursue other options (like XMLHttpRequest).

Playground link: https://runkit.com/kerrishotts/es2015-promises-and-es2017-async-await

For example, let’s consider searching the iTunes Music library for all albums with the words “Doctor Who”. We can use the fetch API which returns a promise:

fetch("https://itunes.apple.com/search?
term=Doctor%20Who&media=music&entity=album")
.then(r => {
if (r.ok) {
return r.json();
} else {
throw new Error("Couldn't obtain information from the
service");
}
}).then(json => json.results.forEach(result => {
console.log(result.collectionName);
})).catch(err => console.error(err.message));
// Console shows:
// [... after a few ms... ]
// "Doctor Who (Original Television Soundtrack)"
// "Doctor Who Series 5"
// "Doctor Who - Series 6 (Soundtrack from the TV Series)"
// etc.

The asynchronous function looks like this, however:

async function searchItunes(forTerm) {
const response = await fetch(`https://itunes.apple.com/search?
term=${forTerm}&media=music&entity=album`);
if (response.ok) {
const json = await response.json();
return json.results.map(result => result.collectionName);
}
throw new Error("Could not retrieve results from service");
}

async function searchForDoctorWho() {
try {
const albums = await searchItunes("Doctor%20Who");
for (const album of albums) {
console.log(album);
}
} catch (err) {
console.error(`Failed to retrieve results: ${err.message}`);
throw err;
}
}

searchForDoctorWho()
.catch(err => console.err(err.message));

Personally I prefer the async / await version for a few reasons:

  • It’s obvious the function is asynchronous — async is there in plain sight. Of course, when using promises with a chain the same is also true, but the function that initially returns the promise doesn’t scream “asynchronous” unless it’s well documented.
  • The logic can flow sequentially within the same function when using await , whereas the promise requires several then callbacks. Being within the same scope is really useful when you need to work with intermediate results. With promises you’d either have to have to close over those intermediate results or you’d have to remember to pass them through the promise chain.
  • Error handling is done via try...catch instead of having to worry about catch handlers. Although, at the top level, I still suggest catching anything the asynchronous function might throw.

Performance

async / await are new enough that there’s not a lot out there regarding their performance, especially compared to promises or even just using callbacks. Based on the bit of information I’ve seen thus far (see https://kyrylkov.com/2017/04/25/native-promises-async-functions-nodejs-8-performance/), however, the ranking seems to be that callbacks are fastest, followed by promises, and then async / await rounds up the rear as the slowest. But that’s based on scant data at the moment, and really only applies to Chrome’s V8 engine and Node.

Even so, I think that order makes sense. Promises do involve a bit more overhead from the JS engine’s perspective than do simple callback pyramids. So, it’s no surprise to me that they are slower. (And the degree of this slow down will vary depending on if you’re using a polyfill or native promises, too.) Since asynchronous functions deal in promises, I wouldn’t expect them to ever be faster than using a promise, and there’s got to be a little bit of overhead from managing the asynchronous function’s internal state, so asynchronous functions being slower than promises makes sense to me as well.

Even so, it’s early days, especially for asynchronous functions (they were just released with the ES2017 standard). Where they make the code clearer, I wouldn’t hesitate to use asynchronous functions. Where promises make for clearer code (or at the top level), I’d use promises.

Oh, and check out the sources for the post — there’s a lot more regarding promises in the sources for this post, so you’ll definitely want to read those in order to get a more complete understanding.

Next Time

Next time we’re going to talk about three features in a single post: Array.from, the “spread” operator, and the “rest” operator. These are pretty cool new features in ES2015, and you’ll want to use them just about everywhere you can. I know I do! Even so, it’s important to understand what’s going on before you do so. I hope you’ll check it out.

Like what I’m doing? Consider becoming a patron!

Creating articles, technical documentation, and contributing to open source software and the like takes a lot of time and effort! If you want to support my efforts would you consider becoming my patron on Patreon?

--

--

Kerri Shotts

JavaScript fangirl, Technical writer, Mobile app developer, Musician, Photographer, Transwoman (she/her), Atheist, Humanist. All opinions are my own. Hi there!