JavaScript Promises: a practical guide
Promises are used to manage asynchronous operations, which were eventually added to JavaScript in the ECMAScript 2015 specification. Promises simplify the previous approach to async actions control, based on the callback function. This issue is of great significance to anyone working in JavaScript – or planning to do that. I hope this text will clarify the subject to you, as I’m going to give you a detailed description of what “promises” are and how to make good use of them.
Table of contents
Asynchronicity in JavaScript
JavaScript is a single-threaded language, which means that an app can only perform one action and then move on to the next one. An example of a synchronous action would be waiting in a line in a store. You can’t finish your shopping before each person preceding you is served.
Ordering in a restaurant is an example of an asynchronous action. You go to, let’s say, a pizzeria and order your meal, which is then prepared by a cook. In the meantime, other people can place their orders, they don’t need to wait for you to get your pizza. Everyone will have their food as soon as it’s ready.
Picture it this way: your JavaScript app is synchronous. As you’re expecting to perform an action – for instance, order a pizza – the person before you is being served, and you (meaning your app) have to wait for them to be done with it. You can place your order only when the aforementioned action is finished: as long as you don’t starve before that happens.
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log('Another action');
Imagine what would happen if some time-consuming actions were looped. Your code would be blocked all along. This is where asynchronicity comes in handy. Namely, you’re able to give your app a task to be performed “in the background”. Your app remains available for other actions, and when the first action is completed, you get feedback containing data and the state of the action.
One of the most commonly performed asynchronous actions in JavaScript apps is fetching data from external servers.
What is a Promise Object in JavaScript?
Promise objects represent a “promise” of completing the requested action and of delivering its value. Why is it actually called a “promise”? Because you never know when the action is going to be completed – or if it gets completed at all.
Have a look at what promises look like in practice. Promises can exist in several states:
Promise object states:
fulfilled/resolved – meaning successfully handled,
rejected – meaning unsuccessful processing of the promise,
pending – meaning the initial state, neither resolved nor rejected.
A promise might remain pending forever. That won’t do any harm to your app: there simply won’t be any response to your action and the promise object will take up space in the memory.
After the state changes to settled – the promise is resolved – you can’t switch it to anything else. In other words, if the promise is in the resolved state, you can’t reset it to pending or rejected.
Okay, it’s high time we did some coding!
For the purpose of our experiment, we’re going to use a public API, JSONPlaceholder. We’ll be working on a real-life example to show you how to implement this solution in your app.
The following list presents the issues that we’re going to tackle further in the article:
Promise.prototype.then() + Promise chain
Promise.prototype.catch()
Promise.prototype.finally()
Promise.resolve()
Promise.reject()
Promise.race()
Promise.all()
Promise.prototype.then() + chaining in JavaScript
Let’s get to work! We want to download the todos list from our API.
const todos = fetch('https://jsonplaceholder.typicode.com/todos');
console.log('TCL: todos', todos);
Take a look at the console:
Promise {<pending>}
It turns out we haven’t received the result of our query but an object representing a future value (promise). To handle HTTP queries, the fetch() method uses promises. It’s now time to address the values returned by the promise and that’s where we’re helped by the then() method. It determines what is supposed to happen at the moment when the promise is successfully resolved.
The then() method contains two arguments. The first one is the function which is meant to be realized when the promise is fulfilled. The other argument is the function which should be realized in case the promise is rejected.
Let’s see how this function looks in practice:
const todos = fetch('https://jsonplaceholder.typicode.com/todos');
todos.then(
response => console.log('TCL: response', response),
err => console.log('TCL: err', err)
);
These are the results of the code and the requested data:
{
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: ""
type: "cors"
url: "https://jsonplaceholder.typicode.com/todos
}
Promise Chain in JavaScript
And what if you want to use the data in some way, e.g. to process them to make them more legible?
This is where chaining comes in.
Chaining means performing then() functions one by one. In practice, the following function will only be performed after the preceding then() function has been completed.
Chaining is possible because the then() function returns the promise.
const todos = fetch('https://jsonplaceholder.typicode.com/todos');
todos
.then(response => response.json())
.then(json => console.log(json));
In response, you receive data which is ready to use. This is how function chaining works.
[
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
},
{
"userId": 1,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"completed": false
},
...
]
Note: A frequent error which occurs when the function chaining is used consists in the failure to return the previous value. This happens quite often, especially when it comes to operations which are nested and linked. Sometimes it’s pretty difficult to detect, but you need to keep it in mind.
Our case is quite simple so it’s really easy to find the error in the code.
const todos = fetch('https://jsonplaceholder.typicode.com/todos');
todos
.then(response => {
response.json();
})
.then(json => console.log(json));
\`undefined\`
Error handling in the then() function
Now, let’s concentrate on the parameter of the then() function, i.e. the case when something goes wrong, e.g. when you provide an incorrect URL address.
First, you need to find out what will happen if the error isn’t handled.
const todos = fetch('https://jsonMISTAKE.typicode.com/todos');
todos
.then(response => console.log('TCL: response', response));
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
Uncaught (in promise) TypeError: Failed to fetch
As you can see above, there’s an error in the code. Sometimes, unhandled code may stop the whole application from working. Since we don’t want that situation to take place, we must check how the possible error can be handled.
const todos = fetch('https://jsonMISTAKE.typicode.com/todos');
todos.then(
response => console.log('TCL: response', response),
reject => console.log('Error: ', reject)
);
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
Error: TypeError: Failed to fetch
This way, we managed to handle the error in our app. Of course, this is just an example and errors should be handled in a more user-friendly manner.
The approach to error handling by means of the second argument of the then() function has certain limitations. If you write several subsequent then() methods, you will have to add error handling to each single performance of the then() function.
In some cases, this possibility may be useful, but normally it is enough to write one method that will handle the possible error. In the next paragraph, I will tell you more about the catch() method, but for now, let me show you the above mentioned limitations resulting from error handling through the then() function second argument.
const todos = fetch('https://jsonMISTAKE.typicode.com/todos');
todos
.then(
response => response.json(),
reject => console.log('Error: ', reject))
.then(json => json.map(item => item.title))
.then(todosTitle => console.log(todosTitle));
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
Error: TypeError: Failed to fetch
Uncaught (in promise) TypeError: Cannot read property 'map' of undefined
at todos.then.then.json
In this scenario, we handled the first error related to downloading but we did not return any data. The second then() function is supposed to process the chart with the todo objects and instead, it informs us about the error. That happened because after handling the error, we returned the undefined function.
Promise.prototype.catch() in JavaScript
In the previous paragraph, I told you a little bit about error handling. Now I’m going to show you another method that can be used to handle errors in the case of asynchronous actions.
Let’s move straight to the code itself. I’m going to use the previous example, which reports an error despite error handling.
const todos = fetch('https://jsonMISTAKE.typicode.com/todos');
todos
.then(response => response.json())
.then(json => json.map(item => item.title))
.then(todosTitle => console.log(todosTitle))
.catch(err => console.error(err));
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
TypeError: Failed to fetch
The error has been handled successfully. No matter where the error occurs in the promise, the catch() method will fetch it and handle it in the predefined way.
Multiple Catch
Now we’re taking a step further. What will happen if we build subsequent catch() methods? Will they be called one by one, like the then() functions, will they be executed all at one point, or perhaps none of them will be executed? Take a look:
const todos = fetch('https://jsonMISTAKE.typicode.com/todos');
todos
.then(response => response.json())
.then(json => json.map(item => item.title))
.catch(err => {
console.warn('Erorr 1', err);
return err;
})
.catch(err => {
console.warn('Erorr 2', err);
return err;
});
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
Erorr 1 TypeError: Failed to fetch
As you can see above, it turns out that only the first catch was called, whereas the other one was omitted. Why did that happen? That was the case because the first catch didn’t return an error, so there was no need to propagate the error further. That is why the other catch() function was not executed.
To invoke the other catch() as well, you need to provoke an error in the first one.
const todos = fetch('https://jsonMISTAKE.typicode.com/todos');
todos
.then(response => response.json())
.then(json => json.map(item => item.title))
.catch(err => {
console.warn('Erorr 1', err);
throw Error('Error from catch 1');
})
.catch(err => {
console.warn('Erorr 2', err);
return err;
});
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
Error 1 TypeError: Failed to fetch
Error 2 Error: Error from catch 1
at todos.then.then.catch.err
So, we managed to run both catch() methods, created one after another. This way, using the catch() method, you’ll be able to handle errors more efficiently.
Promise.prototype.finally() in JavaScript
Another method that can be used in a promise object is the finally() method. If the finally() function has been written for the promise, it will be executed any time the state of the promise changes to settled (either fulfilled or rejected).
How can you use this function?
The finally() method will come in useful when your code is duplicated and you want to use it in the then() and catch() method.
For example: there is a spinner on a website, as an independent element; after realizing the promise, you want to present the information returned by the promise to the user or handle the error and remove the spinner. This is how it looks in practice.
The first example doesn’t use the finally() method:
todos
.then(response => {
showUserSuperSecretData();
removeSpinner();
showDataAboutApiCall();
})
.catch(err => {
superNiceErrorHandler();
removeSpinner();
showDataAboutApiCall();
});
We implemented a couple of things that are meant to be done after the state of the promise changes. However, the code doesn’t look correct as the same methods are repeated in several place. One of the rules for good coding is DRY (Don’t Repeat Yourself). How to make this particular code better? We’ll try to do the same thing using the finally() method.
todos
.then(response => {
showUserSuperSecretData();
})
.catch(err => {
superNiceErrorHandler();
})
.finally(() => {
removeSpinner();
showDataAboutApiCall();
});
In this case, no code is replicated. Thanks to the finally() method, you can be sure that the code it contains will be executed at the end of the promise.
Promise API in JavaScript
In this fragment, I’m going to show you the four static methods of the promise class. Since the methods are static, they can be called directly from the promise class.
Promise.resolve()
The resolve() method immediately returns a resolved (fulfilled) Promise.
const resolve = Promise.resolve('finished');
console.log('TCL: resolve', resolve);
`Promise {<resolved>: "finished"}`
The promise is resolved, but how can that be used in practice?
I will now describe two cases of promises that may come in handy.
- The promise.resolve() method can be used to change a non-standard promise, e.g. jquery Ajax request, into a native promise.
Promise.resolve($.getJSON('https://jsonplaceholder.typicode.com/todos'))
.then(response => showUserSuperSecretData(response))
.catch(err => superNiceErrorHandler(err))
.finally(() => removeSpinner());
- When you use a conditional expression to return the value in the function, you can use the promise class prototype methods in this function.
function fetchPost(id) {
const post = //search our local data
return post ? post : fetchPostPromise(id)
}
fetchPost(id)
.then(() => /\*fancy code \*/ )
.catch(() => /\*fancy code \*/ )
Here, if we find the post in our local resources, we will return a regular value and receive the error below.
Uncaught TypeError: fetchPost(...).then is not a function
That error can be prevented by returning the promise if the post is found in the resources.
function fetchPost(id) {
const post = //search our local data
return post ? Promise.resolve(post) : fetchPostPromise(id)
}
fetchPost(id)
.then(() => /\*fancy code \*/ )
.catch(() => /\*fancy code \*/ )
Promise.reject()
Promise.reject() works analogously to the Promise.resolve() method: it returns the Promise in the rejected state.
Promise.race()
This method accepts promises in an array and returns a promise with the value of the quickest performed promise from a given structure. The response is one promise. Below, you can see an example of how this function works.
const promiseA = new Promise(resolve => {
setTimeout(() => {
resolve('promiseA');
}, 1000);
});
const promiseB = new Promise(resolve => {
setTimeout(() => {
resolve('promiseB');
}, 2000);
});
const fastestPromise = Promise.race(\[promiseA, promiseB]);
fastestPromise.then(response => {
console.log('TCL: response', response);
});
This function is seldom used, but I managed to find an example of how to put it into practice. While creating a promise, you don’t know how long it will take to become resolved (settled) or whether it will ever change its pending state.
You can make use of the race() method to determine the upper limit of performing the promise, e.g. an HTTP query. When the time limit is exceeded, another action will be performed, for example informing the user of the prolonged waiting time.
That’s how it looks in the code:
function timeoutPromise(ms, promise) {
const timeoutErrorPromise = new Promise((resolve, reject) => {
setTimeout(
\ () => reject(Error(\`Operation timed out after ${ms} milliseconds\`)),
\ ms
);
});
return Promise.race(\[promise, timeoutErrorPromise]);
}
const imagePromise = fetch('https://source.unsplash.com/user/erondu/1600x900');
timeoutPromise(100, imagePromise)
.then(response => console.log(response))
.catch(err => console.error(err));
Error: Operation timed out after 100 milliseconds
You can see the function is working, but it returned the error after prolonged time out. Obviously, the time shown above is just an example.
Promise.all()
It often happens that it’s necessary to retrieve data from a couple of sources in apps and then operate on them from different places. In other words, sometimes you want to wait for all the data and then work on them. One of the ways of doing this is obtaining each new data portion after receiving the previous one: the sequential retrieval. However, this technique will considerably prolong the operation.
fetch('https://jsonplaceholder.typicode.com/todos')
.then(todos =>
fetch('https://jsonplaceholder.typicode.com/users').then(users => {
\ console.log('TCL: users', users);
\ console.log('TCL: todos', todos);
})
)
.catch(err => {
console.warn('Something went wrong: ', err);
});
Data retrieval 1 -> Data retrieval 2 -> Operation on the retrieved data
There is a better way to cope with that. The all() method is very helpful in this situation. The promise.all() method adopts the promises array as a parameter. After resolving all the promises, promise.all() responses by returning the promise object along with the value array returned from all the promises.
const promises = Promise.all([
fetch('https://jsonplaceholder.typicode.com/todos'),
fetch('https://jsonplaceholder.typicode.com/users'),
]);
promises
.then(result => {
console.log('TCL: result', result);
})
.catch(err => {
console.warn('Something went wrong: ', err);
});
[
{
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: ""
type: "cors"
url: "https://jsonplaceholder.typicode.com/todos"
},
{
body: (...)
bodyUsed: false
headers: Headers
\_\_proto\_\_: Headers
ok: true
redirected: false
status: 200
statusText: ""
type: "cors"
url: "https://jsonplaceholder.typicode.com/users"
}
]
Promise.all() returned the results ordered by the promises as an argument for the all() method. You can notice that the queries were sent simultaneously, thanks to which the results get back quicker. Each promise is resolved independently.
But what if one of the promises is rejected?
In that case, promise.all() will return the rejected state with the value of the first rejected promise.
const promises = Promise.all([
fetch('https://jsonMISTAKE.typicode.com/todos'),
fetch('https://jsonplaceholder.typicode.com/users'),
]);
promises
.then(result => {
console.log('TCL: result', result);
})
.catch(err => {
console.warn('Something went wrong: ', err);
});
GET https://jsonmistake.typicode.com/todos net::ERR_NAME_NOT_RESOLVED
Something went wrong: TypeError: Failed to fetch
JavaScript Promises – Summary
If you want your apps to function better and in a more efficient way, it’s a good idea to dig deeper into the subject of asynchronicity. This article shows how you can deal with asynchronicity based on HTTP queries.
Thanks to these functions, your code will work faster because it allows you to perform a couple of tasks in the background without blocking the operation of the whole app. This, in turn, will translate into better UX in the app.
If you have some time to spare, take a look at this mini quiz where you can answer a few questions concerning asynchronicity. Beware – some questions are really tricky!
To sum up, I’d like to mention the new async/await syntax, which was introduced to JavaScript in the ES8 version.
The async/await syntax is the so called “syntax sugar”, which is designed to facilitate asynchronous coding. After using this syntax, your code will look more synchronous.
To find out more about the async/await syntax, take a look at these links:
Share this article: