We encounter promises in real life every now and then. You have promised to deliver that project within the next week. Your friend has promised to play Overwatch with you tonight. If you think about it, promises are everywhere around us. Promises in JavaScript also play similar roles. A Promise object in JS is an object that promises to come up with a value or in the case of any error, a reason for the failure. But we don’t know when the promise will complete, so we attach callbacks to the promise object which get called on value or error.
Why Promises are useful?
Of course before we can dive into the technicalities of promises, you will have this question. Why does Promise matter in the first place? What is the use case? If you have written any JS code that fetches some data over the internet, you may have already got used to the fact that JavaScript is single threaded and uses asynchronous operations in places. To deal with asynchronous JS parts, we are long used to using callbacks and event listeners.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
function downloadImage(imageURL, callback) { // Some network requests here which takes time, let's use setTimeout as an example const error = null; const result = "image data"; setTimeout(() => callback(error, result), 3000); } // How we pass callbacks downloadImage("some url", (error, result) => { if (error) { console.log("Error") } else { console.log(result) } }); |
This looks good but if you have written a few level of nested callbacks, you will soon find out the callback hell is real. You may also architect a few Pyramids of Doom.
Promises are one of clean ways to solve this problem. Without getting into technical parts, we can rewrite the download image example using Promises like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
function downloadImageWithPromise(imageURL) { // Some network requests here which takes time, let's use setTimeout as an example const error = null; const result = "image data"; return new Promise((resolve, reject) => { setTimeout(() => { if (error) { reject(error); } else { resolve(result) } }, 3000) }) } downloadImageWithPromise("some url").then(console.log).catch(console.error); |
In most cases, we will be consumers of promises, so don’t worry if the downloadImageWithPromise
doesn’t immediately make sense. We will dive into promise creation soon. For now, take a look at how easy it is to consume a promise. No more callbacks or headaches. The code is clean, easy to reason about and should be easy to maintain in the long run.
With the latest JS changes, some of the important APIs are also based on promises. So it’s very essential that we understand the basics of Promises before hand.
Making a Promise
If you were a little confused about the downloadImageWithPromise
function, worry no more, we will break it down now. And hopefully, it will no longer remain confusing. The basic idea of making promises in JavaScript that when we don’t immediately have a value to return from our function (for example an async operation), we should return a promise instead. So the user / consumer can rely on that promise and retrieve the value from it in the future. In our code, when the async operation is complete, we should “settle” the promise object we returned. Settling means either resolving / fullfilling or rejecting the promise with an error.
Creating a new promise object is simple. We use the new
keyword with the Promise
constructor. We pass it an “executor” function. The executor function takes two arguments – a resolve
callback and a reject
callback. We run our async operations within this executor function and when the operation completes, we either call the resolve
or reject
callback with appropriate values.
|
function iPromiseValue() { return new Promise(executor); } function executor(resolve, reject) { setTimeout(() => resolve("Here's your value!"), 3000) } |
To make things clearer, we separated our executor function. This is basically how promise works. The promise constructor takes a executor function. The executor function takes two callback, resolve and reject. As soon as one of these callbacks is called, the promise is settled and the value (or the error) is made available to the consumer.
Consuming a Promise
Promise objects have two convenient methods – then
and catch
– we can pass callbacks to these methods which will be later called in order. When these callbacks are called, we will get the values passed to our callbacks. Let’s take a quick example:
|
function getValueAfterDelay(delay, value) { return new Promise((resolve, reject) => { setTimeout(() => resolve(value), delay); }) } getValueAfterDelay(3000, "the value") .then((value) => { console.log("Got value: " + value) }); |
And here’s an example with rejection:
|
function rejectAfterDelay(delay) { return new Promise((resolve, reject) => { setTimeout(() => reject("No!"), delay) }) } rejectAfterDelay(3000) .then((value) => console.log("Did we get a value? :o")) .catch(console.error); |
Chaining Callbacks
The then
method returns a promise. So we can keep chaining multiple promises one after one.
|
function getMePromise(value) { return Promise.resolve(value); } getMePromise(2) .then((value) => 2 * value) .then((value) => value + 1) .then(console.log); |
First things first – the Promise.resolve
and Promise.reject
methods immediately return a resolved or rejected promise with the value we pass. This is convenient where we need to return a promise but we don’t need any delay, so need for an executor function and the separate callbacks. We can just do Promise.resolve
or Promise.reject
and be done with it.
We can see how we chained multiple then
methods and passed multiple callbacks to gradually transform the values. If we return a promise from one of this callbacks to then
methods, it will be settled before passing on to the next callback.
Please note: If you look at the Promise related docs on MDN or some other places, you will find that the then
method can take two callbacks, one for success and one for failure. The catch
method is simply then(undefined, errorHandler)
in disguise. But there’s a problem with using two callbacks to the then
method. Take a look at this example:
|
function getMePromise(value) { return Promise.resolve(value); } function successCallback(value) { if (value < 10) { throw new Error("Less than 10"); } } function failureCallback(err) { console.log("Error:" + err); } getMePromise(2).then(successCallback, failureCallback) |
Running the code will get us an error: UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Less than 10.
So what’s happening here? The error callback in a then
method gets called only if there’s any error in the previous step (in this case the getMePromise
function). But it can not handle the error caused in the same level (from within the successCallback
function). This is why a then().catch()
chain works better than passing two callbacks to the then
method itself.
Promise in Real Life
We have got some basic ideas about Promise. Now we will see a real life example of using promise. We would use the axios
npm package to fetch a web page content. This package has a nice promise based API. Let’s install it first:
Now we can use the package.
|
const axios = require("axios"); axios.get("http://google.com") .then((resp) => console.log(resp.data.length)) .catch((error) => console.error(error)); |
The axios.get
function makes a HTTP GET request to the URL provided and returns a promise. We then attach success and error callbacks.
Multiple Promises
Sometimes we may need to deal with multiple promises. We can do that using Promise.all
. It takes a list (iterable) of promises and returns a single promise that we can track. This single promise is resolved when all the promises in the list has resolved or one has failed (whichever happens first). It will return the results of the resolved promises in the same order. Let’s see an example:
|
const axios = require("axios"); const googlePromise = axios.get("http://google.com"); const facebookPromise = axios.get("http://facebook.com"); const allPromises = Promise.all([googlePromise, facebookPromise]); allPromises .then(([googleRes, fbRes]) => console.log(googleRes.data.length, fbRes.data.length)) .catch((error) => console.error(error)); |
Here we create two promises separately, put them in a list and pass it to Promise.all()
. Then we attach our callbacks to the new promise we got. Note how we got two results in the same order inside the callback to the then
method.
There is another convenient method – Promise.race()
which is similar but settles as soon as one of the passed promises is resolved or rejected.
Migrating from Callback
In most cases, you can use various utilities available to convert callback based APIs to promise based one. In case, it’s not possible, just wrap their APIs in your own promises. For example, the popular request
package on npm has a little different callback syntax. So we can wrap it in our own promise like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
const request = require("request"); function makeRequest(url) { return new Promise((resolve, reject) => { request(url, function (error, response, body) { if (error) { reject(error); } else { resolve(body); } }); }); } makeRequest('http://www.google.com').then(console.log); |
In this case, we make the request
call from inside an executor function and return the promise. We pass a callback to the request
function just like it expects. Inside the callback we make use of our resolve / reject callbacks to settle the promise.
Further Reading