Sitemap

Understanding Promise in Javascript

9 min readDec 15, 2023

--

ES6 introduced the promises. A JavaScript Promise is an object that encapsulates the result of an asynchronous operation that may complete at some point in the future. A promise is a proxy for a value not necessarily known when the promise is created. Promise object contains both the producing code and calls to the consuming code.

In the past, to deal with asynchronous operations, you often used the callback functions. However, when you nest many callback functions, the code will be more difficult to maintain. And you end up with a notorious issue which is known as the callback hell.

A promise object can have 3 states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation has failed.

A promise starts in a pending state, indicating that the asynchronous operation is in progress. If the operation is successful, the state changes to fulfilled and if an error occurs, the state changes to rejected. The eventual state of a pending promise can either be fulfilled with a value or rejected with a reason (error).

Once a promise reaches either fulfilled or rejected state, it stays in that state and can’t go to any other state. A promise that is either fulfilled or rejected is called “settled”.

Creating a Promise

To create a promise object, we use the Promise() constructor. Once a new Promise object is created, its state is pending.

let p = new Promise(function(resolve, reject) {
// executor
console.log("Inside executor!");
});

console.log(p);
Output:
Inside executor!
Promise { <pending> }

The promise constructor accepts a callback function called executor that typically performs an asynchronous operation. When new Promise is created, the executor runs automatically.

The executor accepts two callback functions called resolve and reject. If the asynchronous operation completes successfully, the executor should call the resolve() function to change the state of the promise from pending to fulfilled with a value. If the asynchronous operation fails, the executor should call the reject() function to change the state of the promise from pending to rejected with the error reason. So the executor eventually moves promise to one of these states.

The executor should call only either one resolve or one reject. Any state change is final. All further calls of resolve and reject are ignored.

The promise object returned by the new Promise constructor has 2 internal properties (which are inaccesible directly) - state and result.

  • state — initially pending, then changes to either fulfilled when resolve is called or rejected when reject is called.
  • result — initially undefined, then changes to value when resolve(value) is called or error when reject(error) is called.
let p = new Promise(function(resolve, reject) {
setTimeout(() => {
resolve("Task finished successfully");
console.log(p);
}, 1000);
});
Output:
Promise { 'Task finished successfully' }

Reject example:

let p = new Promise(function(resolve, reject) {
setTimeout(() => {
reject("Task has failed");
console.log(p);
}, 1000);
});
Output:
Promise { <rejected> 'Task has failed' }

Note: We have used the setTimeout() function to simulate the asynchronous operation.

Consuming a Promise: then, catch, finally

Consuming functions can be registered (subscribed) using the methods then() and catch(). These methods are used to associate further action with a promise that becomes settled.

then() method

To get the result of a promise when it’s settled, you call the then() method of the promise object. We can have multiple then() handlers for the same promise. The following shows the syntax of the then() method:

promise.then(
function(value) { /* onFulfilled - runs when promise is fulfilled */ },
function(error) { /* onRejected - runs when promise is rejected */ }
);

The first argument of then() is a function often called onFulfilled that runs automatically when the promise is fulfilled and receives the value. The second argument of then() is a function often called onRejected that runs automatically when the promise is rejected and receives the error. Both onFulfilled and onRejected arguments are optional.

An example of fulfilled promise:

let p = new Promise((resolve, reject) => {
setTimeout(() => resolve("Task finished successfully!"), 1000);
});

// resolve runs the first function in .then
p.then((value) => {
console.log(value); // runs after 1 second
}, (err) => {
console.log(err); // doesn't run
});
Output:
Task finished successfully!

An example of rejected promise:

let p = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Task has failed!")), 1000);
});

// reject runs the second function in .then
p.then((value) => {
console.log(value); // doesn't run
}, (err) => {
console.log(err); // runs after 1 second
});
Output:
Error: Task has failed!
at Timeout._onTimeout (.../test-promise.js:2:29)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7)

catch() method

If you want to get the error only when the state of the promise is rejected, you can use the catch() method of the Promise object. If we’re interested only in errors, then we can use null as the first argument in then(): .then(null, onRejected). Or we can use .catch(onRejected), which is exactly the same. The call .catch(f) is a complete analog of .then(null, f), it’s just a shorthand.
.catch(f) is same as .then(null, f).

let p = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Task has failed!")), 1000);
});

p.catch((err) => {
console.log(err);
});
Output:
Error: Task has failed!
at Timeout._onTimeout (.../test-promise.js:2:29)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7)

finally() method

Sometimes, you want to execute the same piece of code whether the promise is fulfilled or rejected. finally() runs always, when the promise is settled: be it fulfilled or rejected. The idea of finally() is to set up a handler for performing cleanup/finalizing after the previous operations are complete. A finally() callback will not receive any argument.

let p = new Promise((resolve, reject) => {
setTimeout(() => resolve("Task finished successfully!"), 1000);
});

p.then((value) => {
console.log(value);
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log("inside finally");
});
Output:
Task finished successfully!
inside finally

Promise Chaining:

We use promise chaining when we have to execute multiple asynchronous operations in sequence where the next operation starts with the result from the previous operation.

The methods then()and catch() are used for promise chaining because these methods always return a new Promise. Because these methods return a new Promise, you can call then(), catch() and finally()on the returned Promise.

When you return a value using return statement in the callback function of then()and catch() methods, these methods return a new Promise that immediately fulfills with the return value. If no value is returned, result is equal to undefined.

.then() in promise chaining

Promise chaining looks like this:

new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
})
.then((value) => {
console.log("then1:", value);
return value * 2;
})
.then((value) => {
console.log("then2:", value);
return value * 2;
})
.then((value) => {
console.log("then3:", value);
return value * 2;
});
Output:
then1: 1
then2: 2
then3: 4

The idea is that the result is passed through the chain of then handlers. Because the then() method returns a new Promise with a value resolved to a value, you can call the then() method on the return Promise like this. In this example, the return value in the first then() method is passed to the second then() method. You can keep calling the then() method successively as follows.

Get Deepak Kumar Singh’s stories in your inbox

Join Medium for free to get updates from this writer.

If you need to pass the result from the previous task to the next one without passing the result, you use this syntax:

step1()
.then(step2)
.then(step3)
...

Example:

function step1(value) {
console.log("step1:", value);
return value * 2;
}

function step2(value) {
console.log("step2:", value);
return value * 2;
}

function step3(value) {
console.log("step3:", value);
return value * 2;
}

new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(10);
}, 1000);
}).then(step1)
.then(step2)
.then(step3);
Output:
step1: 10
step2: 20
step3: 40

Returning Promise:

Also, you can return a new promise in the then() method, like this:

function getUser(userId) {
return new Promise((resolve, reject) => {
console.log("Getting the user from the database...");
setTimeout(() => {
resolve({ userId: userId, name: "John" });
}, 1000);
});
}
function getServices(user) {
return new Promise((resolve, reject) => {
console.log(`Getting the services of ${user.name} from API...`);
setTimeout(() => {
resolve(["Email", "VPN", "CDN"]);
}, 3 * 1000);
});
}
function getServiceCost(services) {
return new Promise((resolve, reject) => {
console.log(`Calculating the service cost of ${services}...`);
setTimeout(() => {
resolve(services.length * 100);
}, 2 * 1000);
});
}

getUser(100)
.then(getServices)
.then(getServiceCost)
.then(console.log);
Output:
Getting the user from the database...
Getting the services of John from API...
Calculating the service cost of Email,VPN,CDN...
300

.catch() in promise chaining

We can use catch()in a promise chain to catch any errors. catch() will catch errors upto that point where catch() is defined. Generally we should add .catch() at the end of the then() chain, so that we can catch any error in the promise chain.

new Promise((resolve, reject) => {
setTimeout(() => {
reject(100);
}, 1000);
})
.then((value) => {
console.log("then1:", value);
return value * 2;
})
.catch((value) => {
console.log("catch1:", value);
return value * 3;
})
.then((value) => {
console.log("then2:", value);
return value * 4;
});
Output:
catch1: 100
then2: 300

JavaScript Promise Methods:

Lets see some promise methods like Promise.all(), Promise.allSettled(), Promise.race(), Promise.any(), Promise.reject() & Promise.resolve().

Promise.all(), Promise.allSettled(), Promise.race() and Promise.any() static methods all accept an iterable Object (such as an Array) of promises as input and return a single Promise. These methods run the input promises concurrently which means all promises execute almost at the same time, but not in parallel.

Promise.all()

Promise.all() waits for all promises in the iterable to be fulfilled or any one promise to be rejected.

  • The promise returned by Promise.all() fulfills when all of the input promises fulfill (including when the input array is empty or does not contain a promise), with an array of the fulfillment values of all promises. When an empty iterable is passed, the promise returned is fulfilled and the value is an empty array.
  • Promise.all() shows fail-fast behavior, that is, it rejects immediately when any of the input promises rejects or a non-promise throws an error, with this first rejection message / error message. For example, if we pass in two promises that fulfill after a timeout and one promise that rejects immediately, then Promise.all() will reject immediately. It does not depend on if the other promises have fulfilled.
  • It is often used when there are many asynchronous tasks involved that the overall code relies on to work successfully — all of the ones we want to do before we continue to execute the code.
  • This method aggregates the results of multiple promises.

An example of fulfilled:

let p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 1000, "resolved");
});
let p2 = 93; //non-promise
let p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 2000, "resolved2");
});

let p = Promise.all([p1, p2, p3]);

p.then((value) => {
console.log(value);
}).catch((err) => {
console.log(err);
});
Output:
[ 'resolved', 93, 'resolved2' ]

An example of rejected:

let p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("one"), 1000);
});

let p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("two"), 2000);
});

let p3 = new Promise((resolve, reject) => {
setTimeout(() => reject("rejected"), 3000);
});

let p = Promise.all([p1, p2, p3]);

p.then((value) => {
console.log(value);
}).catch((err) => {
console.log(err);
});
Output:
rejected

Promise.any()

Promise.any() waits for any one promise in the iterable to be fulfilled or all promises to be rejected.

  • The promise returned by Promise.any() fulfills as soon as any of the input promises fulfills, with this first fulfillment value.
  • It rejects when all of the input’s promises reject (including when an empty iterable is passed), with an AggregateError containing an array of rejection reasons.
let p1 = Promise.reject(0);
let p2 = new Promise((resolve) => {
setTimeout(resolve, 100, "quick");
});
let p3 = new Promise((resolve) => {
setTimeout(resolve, 500, "slow");
});

let p = Promise.any([p1, p2, p3]);

p.then((value) => {
console.log(value);
});
Output:
quick

Promise.allSettled()

The promise returned by Promise.allSettled() fulfills when all of the input promises settle (including when the iterable passed is empty), with the result as an array of objects that describe the outcome of each promise, in the order of the promises passed regardless of completion order.

Each outcome object has the following properties:

  • status - A string, either fulfilled or rejected, indicating the eventual state of the promise.
  • value - Only present if the promise is fulfilled. It is same as the value that the promise was fulfilled with.
  • reason - Only present if the promise is rejected. It is same as the reason that the promise was rejected with.
let p1 = Promise.resolve(3);
let p2 = new Promise((resolve, reject) => {
setTimeout(reject("foo"), 100);
});

let p = Promise.allSettled([p1, p2]);

p.then((value) => {
console.log(value);
});
Output:
[
{ status: 'fulfilled', value: 3 },
{ status: 'rejected', reason: 'foo' }
]

Promise.race()

The promise returned by Promise.race() settles as soon as any of the input promises settles, with state and result same as the first promise that settles.

let p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});
let p2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "two");
});

let p = Promise.race([p1, p2]);

p.then((value) => {
console.log(value);
});
Output:
two

Promise.reject()

The Promise.reject() static method returns a Promise object that is rejected with a given reason.

let p = Promise.reject("An error has occured!");

p.then((value) => {
console.log("value:", value);
}, (err) => {
console.log("err:", err);
});
Output:
err: An error has occured!

Promise.resolve()

The Promise.resolve() static method takes a value as input and returns a promise object that is fulfilled with the input value. If the input value is a promise, that promise is returned.

let p = Promise.resolve(123);

p.then((value) => {
console.log(value);
});
Output:
123

--

--