This is part of a series where I try to explain through each of 33 JS Concepts.
This part corresponds to Promises.
Background
JavaScript is a single threaded language, which means it can work on only one thing at once.
I'm sure this sounds great, there are no multiple pathways to worry about. But there is another thing we need to be keep in mind. There is a UI thread that is always running in the browser that needs to be responsive at all times. If JavaScript is focused at the UI thread, who would fetch the data from APIs?
I would go into explaining how, but Philip Roberts does an incredible job in his JSConf Talk.
So JavaScript needs to process some stuff asynchronously, without disturbing the single thread. You want to call an API?
- Make the API call
- Let the API call complete on the browser thread.
- Let the browser come back to you with the result.
- Process the result.
Promises are one (easy) way for achieving Step 4, writing code that come back to a line and process the result from a given statement.
What is a Promise?
A promise is essentially a special object (just like functions are objects) that has a then
method on it which is called when an asynchronous operation completes.
While we can define our own promises, most of the time we will be working with promises that is returned from an inbuild function or a third party library. First, let's look at how this operates.
fetch('https://pokeapi.co/api/v2/pokemon/ditto')
.then(response => console.log(response))
.catch(error => console.error(error));
console.log('this comes first though');
Here, we are using the in-build fetch API to call the PokeAPI and get the results. The implementation would be the same even if you use axios or a GraphQL Client.
The fetch API returns a Promise object on which you can call .then
and error
, the function passed to then
is called when the fetch
is completed, catch
is called if it errors out. Remember that the fetch
goes to a microqueue and the next line will be executed afterwards, the then
or error
callback is called only when the other things finish executing (and the fetch obviously).
No, you Promise.
So, how do we create our own functions based on promises?
function serveDish() {
return new Promise(resolve => {
// An asynchronous function that gives you the dish back with a callback.
createDish(function(error, dish) {
if (error) {
throw error;
}
resolve(dish); // completed the operation and resolved the promise.
});
});
}
The constructor new Promise
helps you create new Promises yourself. You might want to do this if you want to convert something that works by callbacks to a Promise. Or even just create one so that it executes asynchronously. If the promise runs into an error, you can just throw a Error
and then catch
the same at the other side.
The other side:
serveDish()
.then(dish => console.log("Here's your dish", dish))
.catch(err => console.error('Things burned because of ', err));
What happens then?
You can pass a callback to to then
that receives the response of resolve
from the Promise
creation as we have already seen.
Let's chain some promises
But the real promise of Promises
lies in the fact that you can chain them.
learnCooking()
.then(recipes => createDish(recipes))
.then(dish => inviteNeighbours(dish))
.then(dish => serveDish(dish))
.then(servedDish => getFeedback(servedDish))
.catch(err => console.error('Something went wrong! 🔥'));
This is called chaining of promises, you can chain any number of promises you want as long as you return a promise from the then before. This is very easy than it seems because primitives are automatically converted to promises. If you want to convert something to promise explicitly, you can always:
Promise.resolve(41);
Lots of Promises to keep.
So far, we saw how we can handle a single promise and chain the results from these. But what if there are different promises and you need to wait for all of them to complete before you can execute? This is where Promise.all
comes into play.
For eg. you have made multiple dishes studying the recipes.
const dishes = ['names', 'of', 'your', 'favorite', 'dishes', 'go', 'here'];
// map returns the promise and forms dishesPromise array
const dishesPromise = dishes.map(dish => createDish(dish));
// You want to wait for all dishes to complete cooking before you invite neighbours
Promise.all(dishesPromise).then(dishes => inviteNeighbours(dishes));
Promise.all
takes an array of promises as arguments and the then
on it, is executed when all of them completes. The argument to then
callback has an array of results from all the promises.
While Promise.all
is the most used, there also other methods that help you handle multiple promises and take actions accordingly. Find all of them at MDN
Note
There is a second argument to then
that has been neglected for the purpose of this article. It returns the result when a Promise is rejected. It is seen very infrequently, if you would like to reject a promise, it is always better to throw an error as the end user can chain promises and catch
all the errors with a single catch block. Adding multiple functions to then
is confusing.
Top comments (3)
I know this is an intro, but nested promise chains within an
Array#map
is the bee's knees. Hopefully, later on you can show some examples of those, includingPromise#race
,Promise#any
, andPromise#finally
.sure, that is on the cards.
Oh, well done, excellent article, cosine and complete!