There is a good chance that you are already using promises in your code – they are everywhere now. Have you ever considered how they work, though? I this article I want to explain why they are a good thing and what problems do they solve. I will do that while implementing quite a ludicrous sorting algorithm. Let’s start!
Callbacks
The basic concept to understand here is that code in JavaScript can be asynchronous. I explained what does that mean and how it differs from multiple threads in my other article:
Imagine making an Ajax request to fetch some data from the server. You won’t get a response immediately – you need to wait a little bit for the server to respond. What happens then is that other parts of your code are running while waiting for the response. If it weren’t for that, the interface would freeze, waiting with interpreting the rest of your code to the moment you get the response back
It wasn’t always dealt with using promises. Back in the old days, we used callbacks. They are functions that we set to be called when the specified action is finished.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function isInteger(number){ return Math.ceil(number) === number; } function sort(numbers, onSuccess, onFailure) { const sortedNumbers = []; for(let number of numbers) { if(isInteger(number)){ setTimeout(() => { sortedNumbers.push(number); if (sortedNumbers.length === numbers.length && onSuccess){ onSuccess(sortedNumbers); } }, number); } else { onFailure('Only integers allowed!'); break; } } return sortedNumbers; } const numbers = [5, 3, 2, 4]; sort(numbers, sortedNumbers => { console.log(sortedNumbers); // [2, 3, 4, 5] }, reasonOfFailure => { console.error(reasonOfFailure); }); |
In this example, I created a function called sort that accepts three arguments: an array of numbers and two callbacks. It creates a new array of numbers and uses setTimeout to wait with pushing the numbers to it: the bigger the number, the longer it waits. When it is finished, the onSuccess callback is called. If there are any non-integer numbers found (it won’t work properly for non-integers, unfortunately), the function calls the onFailure callback. In the process, we get a new, sorted array! A condition sortedNumbers.length === numbers.length will be met only if all of the numbers are valid non-integers.
The problem
Callbacks are far from being perfect, though. Imagine wanting to do a sequence of operations using callbacks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function onFailure(reason) { console.error(reason); }; const numbers = [5, 3, 2, 4]; sort(numbers, (sortedNumbers) => { sortedNumbers.push(7,1,6); sort(sortedNumbers, (reallySortedNumbers)=> { reallySortedNumbers.push(10,21,11,17); sort(reallySortedNumbers, (evenMoreSortedNumbers) => { evenMoreSortedNumbers.push(43,22,27); sort(evenMoreSortedNumbers, (finallySortedNumbers) => { console.log(finallySortedNumbers); // [1, 2, 3, 4, 5, 6, 7, 10, 11, 17, 21, 22, 27, 43] }, onFailure); }, onFailure); }, onFailure) }, onFailure); |
What we have here is a classic example of callback hell. Doesn’t really look good, does it?
Promise
Its name actually tells us a lot about it. You can think about it as a promise of a future value, a representation of a value not yet available. It can be in one of three states:
- Pending – the value is not available yet
- Fulfilled – the value became available
- Rejected – error prevented the value from being determined
An often brought up analogy is a situation in a restaurant: imagine ordering a cup of coffee. Right after you pay for it, its status is set to pending, and you are promised a coffee in a few minutes. You wait for your cup, doing some other stuff in the meantime, like watching funny cat videos on the web (you are not wasting your time, just like a web browser). Then a few things can happen: you can either get your coffee (what would set the status of said promise to fulfilled), or receive a sad news with an explanation, that they run out of coffee grains, for example, setting the status of a promise to rejected.
A good example of how the promises work is an implementation of a sleep function that is available in many other programming languages, such as C++.
1 2 3 4 5 6 7 8 9 10 11 |
function sleep(time){ return new Promise((resolve, reject) => { if(isInteger(time)){ setTimeout(() => { resolve(); }, time); } else { reject("Time needs to be an integer"); } }) } |
As you can see, the promise constructor takes a function. Its arguments are resolve and reject, which are both functions also. First is called when a promise is fulfilled, the second one – when it is rejected.
Promise .prototype .then
It takes up to two arguments. The first one is a callback function that is going to be called on success. The second one is a callback function for failure. Then function returns a promise.
1 2 3 4 5 6 |
sleep(500) .then(() => { console.log("I've waited for 500 milliseconds"); }, reason => { console.error(reason); }) |
Promise.prototype.catch
It deals with rejections. It also returns a promise.
It behaves the same as calling Promise.prototype.then(undefined, onRejected)
(in fact, calling obj.catch(onRejected) internally calls obj.then(undefined, onRejected)).
It means that catch() is basically the same as then() with only second argument provided.
1 2 3 4 5 6 7 |
sleep(500) .then(() => { console.log("I've waited for 500 milliseconds"); }) .catch((reason) => { console.error(reason); }); |
Keep in mind that then(resolveHandler).catch(rejectHandler) is not the same as then(resolveHandler, rejectHandler) .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
sleep(500) .then(() => { throw new Error('something went wrong'); }, reason => { console.error(reason); // error not caught }) sleep(500) .then(() => { throw new Error('something went wrong'); }) .catch((reason) => { console.error(reason); // error caught }); |
In this example, the first error won’t be caught, because the failure callback is only for the sleep() function.
Chaining promises
We can easily imagine a situation in which we need to make a few async calls one after another. I’m using Fetch API here.
1 2 3 4 5 6 7 8 9 10 11 12 |
fetch(`${apiUrl}/users`) .then(usersResponse => { console.log('The users are fetched'); fetch(`${apiUrl}/companies`) .then(companiesResponse => { console.log('The companies are fetched', companiesResponse); companiesResponse.json() .then(companiesData => { console.log(companiesData); }); }) }) |
It is starting to look like a callback hell again, isn’t it? But I’ve just said that both then and catch return promises. Thanks to that, we can call those functions again, creating a chain.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const usersPromise = fetch(`${apiUrl}/users`) .then(usersResponse => { console.log('The users are fetched'); return fetch(`${apiUrl}/companies`); }) const companiesPromise = usersPromise.then(companiesResponse => { console.log('The companies are fetched', companiesResponse); return companiesResponse.json(); }) const companiesData = companiesPromise .then(companiesData => { console.log('List of companies', companiesData'); return companiesData.length; }); companiesData .then(companiesAmount => { console.log(companiesAmount); // just an amount of companies }); |
We can go even further and don’t store every promise in a variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fetch(`${apiUrl}/users`) .then(usersResponse => { console.log('The users are fetched'); return fetch(`${apiUrl}/companies`); }) .then(companiesResponse => { console.log('The companies are fetched', companiesResponse); return companiesResponse.json(); }) .then(companiesData => { console.log('List of companies', companiesData'); return companiesData.length; }); .then(companiesAmount => { console.log(companiesAmount); // just an amount of companies }); |
That certainly looks better, isn’t it?
In the result, first, the users will be fetched. When that it’s finished, the companies will be fetched. It is important to notice that a return value of a callback is not the same as a return value of a then function itself: what you return from your callback is then wrapped in a new promise. Example of that is returning companiesData.length above, and calling .then() again.
Remember to add return keywords before fetching more data in callbacks in those cases: no doing it would result in the next callback being called before the data is fetched.
1 2 3 4 5 6 7 8 |
fetch(`${apiUrl}/users`) .then(usersResponse => { console.log('The users are fetched'); fetch(`${apiUrl}/companies`); // return missing here }) .then(companiesResponse => { console.log(companiesResponse === undefined); // true }) |
Promise.all
It is also very easy to run a few promises in parallel and wait for all of them to be fulfilled. The solution to this is Promise.all. It takes an array of promises (that will run in parallel) and returns a single promise that will be resolved when all of the promises fron an array are resolved.
1 2 3 4 5 6 7 |
Promise.all([ fetch(`${apiUrl}/users`), fetch(`${apiUrl}/companies`)]) .then(responses => { console.log('users', responses[0]); console.log('companies', responses[1]); }); |
In this example, the users and companies will be shown in the console when both of them are fetched.
The solution
With all that knowledge, we can rewrite our sort function to use promises.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function sort(numbers){ const sortedNumbers = []; return Promise.all( numbers.map( number => sleep(number) .then(() => { sortedNumbers.push(number)}) ) ) .then(() => sortedNumbers); } const numbers = [5, 3, 2, 4]; sort(numbers) .then(sortedNumbers => { sortedNumbers.push(7,1,6); return sort(sortedNumbers); }) .then(sortedNumbers => { sortedNumbers.push(10,21,11,17); return sort(sortedNumbers); }) .then(sortedNumbers => { sortedNumbers.push(43,22,27); return sort(sortedNumbers); }) .then(sortedNumbers => { console.log(sortedNumbers); // [1, 2, 3, 4, 5, 6, 7, 10, 11, 17, 21, 22, 27, 43] }) .catch(reasonOfFailure => { console.error(reasonOfFailure); }); |
And this is how we got rid of a callback hell. If anywhere along the chain you pass a non-integer value such as 0.5 , all of the then() callbacks below the failure would be omitted and it would go straight to the catch() callback.
Other details to keep in mind
Using catch, Promise.resolve and Promise.reject
In real situations, you should not forget to add catch() and handle errors, especially if you are communicating with some API. Remember that it returns a promise too and you can use that.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fetch('${apiUrl}/users') .then(users => users.json()) .catch(failureReason => { console.error(failureReason); const catchedUsers = getCatchedUsers(); if(catchedUsers){ return Promise.resolve(catchedUsers); } else { return Promise.reject('No users available'); } }) .then(usersData => { console.log('These users are either fetched or catched'); }); |
In this example, if fetching users fails, the function tries to use some data that might have been cached before. If none are found, then catch() returns Promise.reject() which will cause the last callback not to be called because a newly returned promise will be rejected at once. In this case, I am also using Promise.resolve because function getCatchedUsers is not asynchronous and to call then() we need a promise. Returning Promise.resolve(catchedUsers) will case a new promise to be returned and immediately resolved.
Adding callbacks to promises long after they are resolved
This might be a little surprising to you. Check this example out:
1 2 3 4 5 6 7 8 9 |
const usersPromise = fetch('${apiUrl}/users') .then(users => users.json()); setTimeout(() => { // the users are fetched by now usersPromise.then(usersData => { console.log(usersData); }) }, 1000); |
As you can see, you can add callbacks using then() and catch() even after the promise itself was resolved or rejected.
Promises fall through
There is an interesting catch to promises. Examine this example:
1 2 3 4 5 |
Promise.resolve('dog') .then(Promise.resolve('cat')) .then(result => { console.log(result); // 'dog' }); |
If you think the result will be “cat“, you are mistaken! Passing a non-function (such as promise, for example) to then() function, causes it to be interpreted as then(null) . It will cause the previus promise’s result to fall through. An easy fix to that is this:
1 2 3 4 5 |
Promise.resolve('dog') .then(() => Promise.resolve('cat')) .then(result => { console.log(result); // 'cat' }); |
Promise .prototype .finally
It is quite a new function (it reached stage 4 in January this year). Passing a callback to it will cause it to be run whether the promise was fulfilled or rejected. It means that finally is similar to calling then(onFinally, onFinally) . It also returns a promise.
1 2 3 4 5 6 7 8 |
let isLoading = true; fetch(`${apiUrl}/users`) .then(onSuccess) .catch(onFailure) .finally(() => { isLoading = false; }); |
In this example, isLoading will always be set to false at the end.
Summary
Although this sorting algorithm is not at all suitable for real-life use, I hope you learned something along the way. Async and promises can be tricky, but it is such an important feature of the JavaScript language, that we can’t escape from it. Embrace it, then!