Understanding JavaScript Promises
Handle asynchronous operations with Promises

Introduction
Before promises, we used callback functions to handle asynchronous operations. Asynchronous operations can include fetching data from an API, reading a file, or using a timer (setTimeout). However, callbacks have limitations. Promises overcomes those limitations.
So, let's understand promises and learn how to use it.
What is a Promise in JavaScript?
A promise is an object that handles asynchronous operations. It ensures you will get a single response (either success or error).
A promise can be in one of three states: Pending, Fulfilled, or Rejected. Throughout its life cycle, a promise begins in the Pending state and then transitions to either the Fulfilled or Rejected state.
The states of a JavaScript Promise are:
Pending: When a promise is initialized using the executor function.
Fulfilled: When an asynchronous operation is successful.
Rejected: When an error occurs, such as an exception during code execution or a network error.
Syntax of a Promise
let promise = new Promise(executor)
.then((success) => {
// When task completes.
})
.catch((error) => {
// When some error occurs and task fails.
});
// for details check promise implementation section on this page.
Once the promise has been Fulfilled or Rejected, it cannot switch to any other state. When we say that a promise has settled, we mean it has reached either the Fulfilled or Rejected state.
JavaScript does not expose the state of promises directly. Instead, we should treat a promise as a black box. Only the function that creates the promise knows it's state. When the promise settles, it informs us by executing handlers. The handler functions are the success handler and the error handler.
How to implement a Promise in JavaScript?
1) Initialize a Promise.
let promise = new Promise(function (resolve, reject) {
// this is the executor function
setTimeout(function () {
resolve("kudos");
}, 3000);
});
The executor function has resolve and reject arguments, which are success and error callbacks. Whichever argument is called first, the promise settles into that final state.
A promise will be resolved by invoking either the resolve or reject argument. If the operation is successful, call resolve with the result of the operation as the argument; otherwise, call reject with the error object.
2) Attach success and error handler.
// this example is the continuation of step 1.
promise
.then(function (data) {
// this is success handler
console.log("promise resolved. " + data);
})
.catch(function (err) {
// this is error handler
console.log("promise rejected with error: " + err);
});
In the executor function, theresolvecallback indicates success, which triggers thethenhandler. The execution of therejectcallback indicates an error, which will trigger thecatchhandler.
To reject a promise, replace resolve('kudos') with reject('sad') in the executor function from step 1 (Initialize a Promise). Other reasons for rejection can include a system error or a network error.
3) What happens when promise settles?
It will either execute a success function then or an error handling function catch.
Promise provides the finally method, which is useful if you need to do some processing or cleanup once the promise is settled.
// this example is the continuation of step 1.
promise
.then(function (data) {
// this is success handler
console.log("promise resolved. " + data);
})
.catch(function (err) {
// this is error handler
console.log("promise rejected with error: " + err);
})
.finally(function () {
console.log("operation complete");
});
Alternative to implement a promise
// Example shows a alternate way to define promises.
let fetchVehicles = new Promise(function (resolve, reject) {
// This is executor function
setTimeout(function () {
resolve("alternative way to define promise");
}, 1000);
});
let successHandler = (vehicles) => {
console.log(vehicles);
};
let errorHandler = (error) => {
console.log("fetching vehicles failed");
};
fetchVehicles.then(successHandler, errorHandler);
The above code represents an alternative way to define promise. This code differs from the previous implementation only by specifying success handler and error handler to then method. In the above code errorHandler handles errors thrown by fetchVehicles and errorHandler will not handle any errors thrown in successHandler function.
The downside of this implementation is that if an error is thrown in the success handler, there is no function to handle the error.
Browser Support
Native support for promise in JavaScript is from ECMAScript 2015 ( ES6 ). To check the browser compatibility for various promise methods across some major browsers then click HERE.
Benefits of using promises over callbacks
Better management of asynchronous requests.
Improves code readability.
Improves error handling.
Warnings while implementing promises
Error handling
catchfunction is required otherwise promise will fail silently.Do not mix traditional callback functions with promises.
Avoid nesting of promises which will make code debugging, review, management a bit difficult. Refer example given below.
getMembersFromAPI()
.then((apiMembers) =>
getMembersFromDB(apiMembers)
.then((members) =>
addBonusToMembers(members).then((data) => {
console.error("add bonus resolved");
})
)
.catch((err) => console.error("add bonus failed: " + err))
)
.then((data) => checkForMembers())
.catch((err) => console.error("Error occurred: " + err));
Shortcut to Settle a promise
Promise.resolve() and Promise.reject() are shortcuts to create an already resolved or rejected promise respectively.
Try below snippet.
let shortcuts_res = Promise.resolve("passed");
shortcuts_res
.then((value) => console.log("Promise is resolved"))
.catch((err) => console.log("Promise is rejected"));
let shortcuts_rej = Promise.reject("failed");
shortcuts_rej
.then((value) => console.log("Promise is resolved"))
.catch((err) => console.log("Promise is rejected"));
Composition tools
Promise.all() and Promise.race() are two composition tools. With the help of these tools, we can execute multiple promises in parallel. Now we will check the implementation of both tools.
1) Promise.all()
If there is a case where we need to fetch data from multiple APIs and process them only if all calls are successful then Promise.all() is a good option.
Consider the following example.
// Below code demonstrate way to execute Promises in parallel.
let promiseA = Promise.resolve("A");
let promiseB = Promise.resolve("B");
let promiseC = new Promise(function (resolve, reject) {
setTimeout(resolve, 100, "C");
});
Promise.all([promiseA, promiseB, promiseC])
.then(([resultA, resultB, resultC]) => {
console.log("result for promise " + resultA);
console.log("result for promise " + resultB);
console.log("result for promise " + resultC);
})
.catch((err) => {
console.log("promise " + err + " failed.");
});
In this example, there is no specific execution order of promiseA, promiseB, promiseC promises. The then handler will be executed only when all promises are resolved or else catch handler will be called. If any one promise fails then error handler will execute and promise will settle.
2) Promise.race()
From the list of asynchronous requests, if we want the result of the first operation which settles to resolve or reject state then Promise.race() will do work for us.
Consider the following example.
// From array of Promises, when we want to the promise which settles down to resolve or reject state first than Promise.race() method is there to do work for us.
let promiseA = new Promise(function (resolve, reject) {
setTimeout(resolve, 400, "A");
});
let promiseB = new Promise(function (resolve, reject) {
setTimeout(resolve, 300, "B");
});
Promise.race([promiseA, promiseB])
.then((value) => console.log("Promise " + value + " is resolved"))
.catch((err) => console.log("Promise " + err + " is rejected"));
Here we are defining two promises promiseA and promiseB. For promiseA, A is argument to resolve callback. For promiseB, B is argument to resolve callback. In this example, promiseB will be resolved first, so then handler will be executed using promiseB result. You can experiment by interchanging settled states.
Chaining of Promises
A good way to execute async operations in series is by chaining all operations using promises. To implement chaining, the then handler should return a promise, which will be handled by subsequent handlers.
To handle errors, we can add a catch at the end after all then handlers, or we can add a catch after each then handler.
let chainA = function () {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve("chainA");
}, 1000);
});
};
let chainB = function () {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve("chainB");
}, 3000);
});
};
let chainC = function () {
return Promise.reject("Error in chainC");
};
chainA()
.then((data) => {
console.log(data);
return chainB();
})
.then((data) => {
console.log(data);
return chainC();
})
.then((data) => {
console.log(data);
console.log("all async request were successful");
})
.catch((err) => {
console.log("common error handle");
console.log("all async request were not successful with error " + err);
});
The above example demonstrates chaining promises and handling errors with a single catch at the end. It shows how to perform tasks in series, making chaining promises a perfect example. To chain all tasks, each task should return a promise. An error handler function should be defined at the end of the chain to catch any errors that go unnoticed. Check the example and experiment with it.
The example below illustrates how to add a handler for each specific task that fails. Also, check the comments in the code.
let chainA = function () {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve("chainA");
}, 1000);
});
};
let chainB = function () {
return new Promise((resolve, reject) => {
setTimeout(function () {
reject("chainB");
}, 3000);
});
};
let chainC = function () {
return Promise.reject("Error in chainC");
};
chainA()
.then(
(data) => {
// success handler for chainA
console.log(data);
return chainB().catch((err) => {
// error handler for chainB
console.log("Error in " + err);
throw new Error(err);
});
},
(err) => {
// error handler for chainA
console.log("Error in " + err);
throw new Error(err);
}
)
.then((data) => {
// success handler for chainB
console.log(data);
return chainC().catch((err) => {
// error handler for chainC
console.log("Error in " + err);
throw new Error(err);
});
})
.then((data) => {
// success handler for chainC
console.log(data);
console.log("all async request were successful");
})
.catch((err) => {
// common error handler for all tasks
console.log("all async request were not successful with error " + err);
});
let chainC = function () {
return Promise.reject("Error in chainC");
};
chainA()
.then(
(data) => {
// success handler for chainA
console.log(data);
return chainB().catch((err) => {
// error handler for chainB
console.log("Error in " + err);
throw new Error(err);
});
},
(err) => {
// error handler for chainA
console.log("Error in " + err);
throw new Error(err);
}
)
.then((data) => {
// success handler for chainB
console.log(data);
return chainC().catch((err) => {
// error handler for chainC
console.log("Error in " + err);
throw new Error(err);
});
})
.then((data) => {
// success handler for chainC
console.log(data);
console.log("all async request were successful");
})
.catch((err) => {
// common error handler for all tasks
console.log("all async request were not successful with error " + err);
});
Things to note about Promises
To reject a promise, we can call the reject function, which is an argument to the executor function, or we can throw an error in the executor function. Whichever method is used first, the promise will settle to that particular value.
let promise = new Promise(function (resolve, reject) {
throw new Error("calculation error"); // throw will reject promise
reject("Reject!");
});
promise
.then(function (value) {
console.log(value);
})
.catch(function (err) {
console.log(err);
});
In the above code example, the promise is going to be rejected by throwing an error.
Conclusion
Promises are the optimal way to manage asynchronous operations. However, the syntax for handling promises can sometimes be cumbersome. The solution to this is to use the async/await syntax, which is essentially syntactic sugar for Promises.
To enhance the readability of this article, you can comment on the lines that should be highlighted.
