webdevelopment

About Promises and async / await

  • 20 January, 2018

Recently I revisited Wes Bos his ES6 for everyone course and made some notes on the Promise and async / await modules. These notes got a little out of hand and now they’ve turned into this huge blogpost. This is in no way meant to be a transcript of ES6 for everyone and I consulted many resources for this article, which you can find at the bottom of this post.

If I got something wrong or missed something important, feel free to correct! You can find this post in my front-end notes github repo.

Promises

ECMAScript 2015 (ES6) brought us the Promise, a native object which act as a proxy (or temporary value) for a value that isn’t known yet. Promises allow you to create handlers that deal with the eventual success or failure of an asynchronous action. In this post I’ll tell you about all the ways to use promises, how to roll your own Promise functions, how to combine and chain promises and how to make the Promise api even better with async / await. But first we’re going to back to the absolute basics: Synchronous and Asynchronous code.

Synchronous / Asynchronous

I’m absolutely sure that most of you can skip this part, but if you do like a small reminder of what kind of asynchronous behaviour we deal with in JavaScript, keep on reading.

When you create functions or objects that immediately return a value, JavaScript seems to be synchronous. It looks like each line is processed sequentially and returns values before the next process starts running.

console.log('this will fire first');
console.log('this will fire second');
console.log('this will fire last');

The above code will return exactly what you’d expect and logs all these lines of text to the console in order.

However, when one of these processes takes longer to return a value than the others we can see that the behaviour is in fact, asynchronous. In the following example we’ll add a setTimeout around the second console.log to clearly see what happens.

console.log('this will fire first');
setTimeout(() => {
    console.log('this will fire second');
}, 500);
console.log('this will fire last');

Now the logging in the console is out of order because the next line will not wait for the previous to finish. To recap:

  • ‘This will fire first’ is logged to the console immediately
  • The setTimeout function is started with 500ms on the timer and will not return a value for that amount of time.
  • ‘this will fire last’ is logged to the console because it doesn’t wait for the result of the setTimeout function.
  • After the 500ms, ‘this will fire second’ is logged to the console.

In our example we obviously add the delay ourselves and as long as we know what the timeout value is, we can deal with the delay. But in many cases we don’t know when something is going to return a value, like when we’re fetching data or deal with a complicated process that takes long before returning a value.

We used to deal with this by using callbacks. A function would take in a callback function which would be called whenever the time consuming process returned something.

function wait(ms, cb) {
    setTimeout(function() {
        console.log(`done after ${ms}ms`);
        cb();
    }, ms);
};

wait(1000, function() {
    console.log('here\'s our callback function');
});

This doesn’t look to bad. But what if we have another function with callback that needs to be called within our callback, and another one within that function? It’s going to get complicated real quickly and it’s what we commonly refer to as callback hell.

wait(300, function() {
    wait(600, function() {
        wait(500, function() {
            wait(400, function() {
                console.log('here\'s our final callback function');
            });
        });
    });
});

All the indenting makes makes the code very hard to read. There were ways around this, but that’s not important anymore, because we’ve got the Promise!

Promise - Introduction

The Promise is a native object which acts as a temporary value for a value that isn’t known yet. Promises allow you to create handlers that deal with the eventual success or failure of an asynchronous action

Native promises

Fetch

Before we’ll start rolling our own promises, let’s have a look on how to work with a promise that is already available in your browser, natively! Since a couple of years fetch has been my go-to api to request data. It’s very clean, easy to remember and to handle. If you didn’t use ajax requests with jQuery, you might remember XMLHttpRequest, which wasn’t a pretty way to get data. Well, no more copy-pasting because you’ll have fetch memorized in no-time. I promise.

console.log(fetch('https://api.github.com/users/aderaaij'));

The code above is still asynchronous. We’re not capturing the result of the promise, we’re just logging the object itself. The result should look something like Promise {<pending>}. This shows you that the user variable is indeed a promise and that the status on the moment of calling console.log(user) was pending. That’s cool and all, but we want to see some data!

To check on the status of a promise, we can tag on a .then method.

fetch('https://api.github.com/users/aderaaij')
    .then(data => console.log(data));

In this little piece of code we’re fetching user data from github and we directly log it when it comes back successful! That was insanely easy, wasn’t it? Now the data we’ve got is still ‘raw’. fetch can be used to retrieve all kinds of data so it doesn’t just assume your data is JSON. Luckily we can convert it to JSON data with the json() method, which returns a promise as well.

Whenever the .then method returns a value, whether a Promise or not, you can tag another .then method right onto it. When you return a value, it’s returned as a promise that immediately resolves: Promise.resolve(val). When you return a Promise, the following .next will be called when the Promise is actually resolved.

fetch('https://api.github.com/users/aderaaij')
    .then(data => data.json())
    .then(data => console.log(data));

As the fetch function returns a promise, we call then on it to transform the data. To turn the data into usable JSON, we call .json() on it. As .json() returns a promise as well, we tag on another .then and can do whatever we want with the transformed data 🎉. You can see how this chaining can be useful if you need to combine multiple functions which are depending on data from functions that might or might not immediately return a value.

But what if there’s an error?

fetch('api.github.com/users/aderaaij')
    .then(data => data.json())
    .then(data => console.log(data));

In the script above we forgot https:// so the browser is now looking for this api in my local files. If you run this, the error should say Uncaught (in promise). This means that we haven’t used the catch method we can tag on to the promised returned by the fetch api.

fetch('api.github.com/users/aderaaij')
    .then(data => data.json())
    .then(data => console.log(data))
    .catch(err => console.error('oh noes:', err));

In this example we’ve added our catch method and we throw an explicit error. The console message should be a bit more informative now.

Finally, we also have the finally method. Finally returns a promise when the original promise is either resolved or rejected. It’s a method you can call when you want something to happen regardless if the function resolves or not. It could be useful to reset a state based on whether the promise has run and finished already. The browser support is still lacking but it could be useful in the future.

fetch('https://api.github.com/users/aderaaij')
    .then(data => data.json())
    .then(data => console.log(data))
    .catch(err => console.error('oh noes:', err))
    .finally(() => console.log('finally'));

getUserMedia

Another cool in-browser promise is the MediaDevices.getUserMedia() method which prompts the user to use a media input like their webcam or microphone and produces a medium stream. Of course this function can’t run without user permission and has to wait for the user to accept to start doing something.

const video = document.querySelector('video');
navigator.mediaDevices.getUserMedia({ video: true })
    .then(mediaStream => {
        video.srcObject = mediaStream;
        video.load();
        video.play();
    })
    .catch(err => console.error(Error("user said no 😡")))

In the above example mediaDevices.getUserMedia({ video: true }) a prompt to ask user permission to get webcam access. When the user either accepts or rejects, the promise resolves or reject. When we accept, we set the mediaStream as sourceObject for our video element, load up the video and play it.

Building your own promises

There are plenty of user cases when we’d want to make our own promises and here I’ll show you how to do just that.

The base of a Promise looks like this:

const p = new Promise((resolve, reject) => {

});
const p = new Promise((resolve, reject) => {
    resolve('yay');
});

It takes in one function called the executor ☠️ with the arguments of resolved and reject. The idea being that whatever the promise returns either resolves and returns some kind of value, or results in an error.

We can execute all kinds of code within our new promise and whenever we’ve got what we want to return and / or reject, we call resolve and reject.

const p = new Promise((resolve, reject) => {
    resolve('This is awesome!');
});

p.then(console.log)

In this case we immediately resolve the value ‘This is awesome!’ When we apply the then method to our promise we immediately get back the value that was resolved.

When you replace resolve with reject, you’d see we will get an uncaught (in promise) error, with the same message. We can also catch this error and give it a little bit more meaning.

const p = new Promise((resolve, reject) => {
    reject(Error('This is an error'));
});

p
    .then(console.log)
    .catch(err => console.error(err));

Now the error is caught and logged. Because we wrapped the reject message in an Error object, we get a lot more information on what might be the problem.

In many cases we would like to return both a reject and a resolve in our promise. For example, you could return a promise in a function that takes in an argument, and reject or resolve based on the argument value.


function isTonyStark(name) {
    return new Promise((resolve, reject) => {
        if (name === 'Tony') {
            resolve(`Welcome ${name}`);
        } else {
            reject(Error('Danger, Will Robinson, danger!'));
        }
    });
}

isTonyStark('Tony')
    .then(console.log)
    .catch(err => console.error(err));

Chaining promises

Promises are very convenient when dealing with multiple processes which will not return something at the same time, especially when those processes might be dependent on each other. With promises you can control the flow and make sure that you won’t execute the next function until the data from the previous one has returned.

So let’s sketch a common scenario where you’ve got two sets of data: A list of movies and a list of heroes. The sets of data come from a database call, so you don’t exactly know when you’ll have it available. You want to make a function that returns both the movie information as some extra info on the main hero.

const movies = [
    { title: 'Thor 3, Ragnarok', company: 'Marvel', hero: 'Thor', id: 1 },
    { title: 'Black Panther', company: 'Marvel', hero: 'Black Panther', id: 2 },
    { title: 'Wonder Woman', company: 'DC', hero: 'Wonder Woman', id: 3 },
];

const heroes = [
    { name: 'Thor', team: 'Avengers' },
    { name: 'Black Panther', team: 'Avengers' },
    { name: 'Wonder Woman', team: 'Justice League', actor: 'Gal Gadot' },
];

So the first thing we’re going to do, is roll our own promise. We want to be able to get a movie by ID, so here we go:

function getMovieByID(id) {
    // We can immediately return a promise in our function, this is how we pass arguments
    return new Promise((resolve, reject) => {
        // Find the movie based on the movie ID
        const movie = movies.find(movie => movie.id === id);
        if (movie) {
            resolve(movie); // Resolve if we've got a movie
        } else {
            reject(Error('oh noes, no movie found'));
        }
    })
}

getMovieByID(3)
    .then(console.log);

And there you have it, the first part of our solution.

For our next step, we need to make another promise so we can chain it onto our getMovieById. When the data is returned, we immediately want to start using it and turn it into something useful.

Our hydrateData function takes in the data object which is returned in the then handler from the getMovieById promise. It then returns the new Promise we’re making.

function hydrateData(data) {
    // Return a new promise
    return new Promise((reject, resolve) => {
        // Find the hero by comparing the `hero` value in the `data` object to `hero.name`
        const info = heroes.find(hero => data.hero === hero.name);
        if (info) {
            data.hero = info; // Assigning info to data.hero (replacing the original `hero` value which was just a string)
            resolve(data);
        } else {
            reject(Error('have no heroes'));
        }
    });
}

getMovieByID(3)
    .then(data => hydrateData(data))
    .then((data) => {
        console.log(data);
    });

And now everything together:

function getMovieById(id) {
    return new Promise((resolve, reject) => {
        const movie = movies.find(movie => movie.id === id);
        if (movie) {
            resolve(movie);
        } else {
            reject(Error('Movie not found'));
        }
    });
}

function hydrateData(data) {
    return new Promise((resolve, reject) => {
        const heroInfo = heroes.find(hero => data.hero === hero.name);
        console.log(heroInfo);
        if (heroInfo) {
            data.hero = heroInfo;
            resolve(data);
        } else {
            reject(Error('oh noe error'));
        }
    });
}

getMovieById(3)
    .then(data => hydrateData(data))
    .then((data) => {
        console.log(data);
    });

Promise.all - Multiple promises

In some cases you want to return multiple promises and wait for all of them to resolve before doing something with that data. In that case you can use Promise.all. .all takes in an array of iterables (promises included) and waits for all of those to be resolved before returning values.

function printThor() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({
                name: 'Thor',
                bff: 'The Hulk',
                team: 'Avengers',
            });
        }, 500);
    });
}

function printQuotes() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(['This drink... I like it!', 'I need a horse!']);
        }, 1000);
    });
}

Promise
    .all([printThor(), printQuotes()])
    .then(([thor, quote]) => console.log(thor, quote));

In this example we’ve got two promises, printThor returns after 500ms, printQuotes after 1000ms. We feed them to Promise.all in an array, and call .then. This returns data when both promises are resolved. To make live easy we destructure both values right in the arrow function arguments.

But what if you fetch data from an array and still need to transform that data to useful JSON? In that case you might want to return yet another Promise.all. This time with a .map function which maps over the responses and returns .json(). Since we’re using .map() which returns an array and .json() which returns a Promise, we basically return an array with promises.

const dog = fetch('https://dog.ceo/api/breeds/image/random');
const dev = fetch('https://api.github.com/users/aderaaij');

Promise
    .all([dog, dev])
    .then(res => Promise.all(res.map(r => r.json())))
    .then(data => console.log(data));

Promises vs Callbacks

If you remember that piece we wrote on

function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(console.log(`waited for ${ms}ms`));
        }, ms);
    });
}

wait(300)
    .then(res => wait(500))
    .then(res => wait(1000))
    .then(res => wait(700))
    .then(res => wait(300))
    .then(res => wait(900))
    .catch(err => console.error(err));

This makes our code a lot more flat and thus readable.

Async / Await

At the core, Async / Await is build on top of promises. In order to async / await anything, you’ll need a function that returns a promise. Await always needs to be called within a function marked with async. There is no top-level await.

function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(console.log(`waited for ${ms}ms`));
        }, ms);
    });
}

const go = async () => {
    await wait(600);
    await wait(1200);
    await wait(1800);
}
go();

In this case we take the exact same Promise we created in the previous code block, and call it in a function marked with async. Just by adding await infront of your function, your code will run synchronously and each wait function waits untill the previous one has resolved. This API is even clearer than the .then method and there seems to be some performance gains as well. At the moment of writing async / await is supported by most browsers.

You can also stick the returned values of a function into a variable:

function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(ms > 500) {
                resolve(`waited for ${ms}ms`);
            } else {
                reject(Error(`you should wait longer than ${ms}ms!`));
            }
        }, ms);
    });
}

const go = async () => {
    const res1 = await wait(600);
    console.log(res1);
    const res2 = await wait(1000);
    console.log(res2);
    const res3 = await wait(1400);
    console.log(res3);
};

go();

In the example above, the result of the first promise gets logged after 600ms while the results of the second and third promises will be logged together after 3600ms.

Error handling

Handling errors with async code blocks is a little bit awkward. You can surround your await statements with a try and catch block like this:

function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(ms > 500) {
                resolve(`waited for ${ms}ms`);
            } else {
                reject(Error(`you should wait longer than ${ms}ms!`));
            }
        }, ms);
    });
}

const go = async () => {
    try {
        const res1 = await wait(600);
        console.log(res1);
        const res2 = await wait(600);
        console.log(res2);
        const res3 = await wait(300);
        console.log(res3);
        const res4 = await wait(600);
        console.log(res4);
    } catch (err) {
        console.error('something went wrong...', err);
    }
}

go();
function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(ms > 500) {
                resolve(`waited for ${ms}ms`);
            } else {
                reject(Error(`you should wait longer than ${ms}ms!`));
            }
        }, ms);
    });
}

const go = async () => {
    try {
        const res1 = await wait(600);
        console.log(res1);
        const res2 = await wait(600);
        console.log(res2);
        const res3 = await wait(300);
        console.log(res3);
        const res4 = await wait(600);
        console.log(res4);
    } catch (err) {
        console.error('something went wrong...', err);
    }
}

go();

In the example above we’ve wrapped all of the await promises in our try block, and if one is rejected we catch that error in our catch block.

You could also make a sort of ‘Higher order component’ which would wrap the go function and would catch all the errors. This is something I totally got from Wes Bos and you should check his talk on async/await in which he goes further into this.

function wait(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(ms > 500) {
                resolve(`waited for ${ms}ms`);
            } else {
                reject(Error(`you should wait longer than ${ms}ms!`));
            }
        }, ms);
    });
}

// First we make a function that takes in our async function as an argument
const catchErrors = (fn) => {
    // And return a function
    return function() {
        // Which returns our async function, which is a promse on which we can call `.catch`
        return fn().catch((err) => {
            console.error('uhoh', err);
        });
    };
};

const go = async () => {
    const res1 = await wait(600);
    console.log(res1);
    const res2 = await wait(600);
    console.log(res2);
    const res3 = await wait(300);
    console.log(res3);
    const res4 = await wait(600);
    console.log(res4);
}

const wrappedFunc = catchErrors(go);

This way we don’t have to write a different try and catch block for each promise. You only want to do that when you need to resolve a lot of promises though, otherwise you might be better off writing your try / catch block and write a bit more specific error messages.

Awaiting multiple promises

You need to be careful with await and multiple promises, it’s not a replacement for Promise.all. Adding an await statement before your promises makes your code truly synchronous, so if you resolve two fetch promises with await, the one won’t start fetching data before the other is finished. In a lot of cases you’d like them to fire off at the same time and wait for the result of both to come back.

async function getDogs() {
    // Store the promise in a variable
    const dog1 = fetch('https://dog.ceo/api/breeds/image/random');
    const dog2 = fetch('https://dog.ceo/api/breeds/image/random');

    const results = await Promise.all([dog1, dog2]); // Wait until both promises are ready
    // Array destructure, await our res.json() promises
    const [mut1, mut2] = await Promise.all(results.map(res => res.json()));
    console.log(mut1, mut2);
}

If we have multiple promises we can even map over them and return them in a Promise.all.

async function getDogs(breeds) {
    const promises = breeds.map((breed) => {
        return fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
    });

    const results = await Promise.all(promises);
    const data = await Promise.all(results.map(r => r.json()));
    console.log(data);
}
getDogs(['husky', 'malamute', 'terrier']);

Fin

That sums it up for now! If you have any questions or remarks, please don’t hesitate to comment or contact me!

Resource list

Did you enjoy this article or would you like to tell me that I'm wrong? Follow me on Twitter or send me an e-mail!