JavaScript now has a new native pattern for writing asynchronous code called the Promise pattern. This new pattern removes the common code issues that the event and callback pattern had. It also makes the code look more like synchronous code. A promise (or a Promise object) represents an asynchronous operation.
Existing asynchronous JavaScript APIs are usually wrapped with promises, and the new JavaScript APIs are purely implemented using promises. Promises are new in JavaScript but are already present in many other programming languages. Programming languages, such as C# 5, C++ 11, Swift, Scala, and more are some examples that support promises.
In this tutorial, we will see how to use promises in JavaScript.
This article is an excerpt from the book, Learn ECMAScript - Second Edition, written by Mehul Mohan and Narayan Prusty.
A promise is always in one of these states:
Once a promise is fulfilled or rejected, it cannot be transitioned back. An attempt to transition it will have no effect.
Suppose you wanted to perform three AJAX requests one after another. Here's a dummy implementation of that in callback-style:
ajaxCall('http://example.com/page1', response1 => { ajaxCall('http://example.com/page2'+response1, response2 => { ajaxCall('http://example.com/page3'+response2, response3 => { console.log(response3) } }) })
You can see how quickly you can enter into something known as callback-hell. Multiple nesting makes code not only unreadable but also difficult to maintain. Furthermore, if you start processing data after every call, and the next call is based on a previous call's response data, the complexity of the code will be unmatchable.
Callback-hell refers to multiple asynchronous functions nested inside each other's callback functions. This makes code harder to read and maintain.
Promises can be used to flatten this code. Let's take a look:
ajaxCallPromise('http://example.com/page1') .then( response1 => ajaxCallPromise('http://example.com/page2'+response1) ) .then( response2 => ajaxCallPromise('http://example.com/page3'+response2) ) .then( response3 => console.log(response3) )
You can see the code complexity is suddenly reduced and the code looks much cleaner and readable. Let's first see how ajaxCallPromise would've been implemented.
Please read the following explanation for more clarity of preceding code snippet.
To convert an existing callback type function to Promise, we have to use the Promise constructor. In the preceding example, ajaxCallPromise returns a Promise, which can be either resolved or rejected by the developer. Let's see how to implement ajaxCallPromise:
const ajaxCallPromise = url => { return new Promise((resolve, reject) => { // DO YOUR ASYNC STUFF HERE $.ajaxAsyncWithNativeAPI(url, function(data) { if(data.resCode === 200) { resolve(data.message) } else { reject(data.error) } }) }) }
Hang on! What just happened there?
You can return a promise in a then call. When you do that, you can flatten the code instead of chaining promises again.For example, if foo() and bar() both return Promise, then, instead of:
foo().then( res => { bar().then( res2 => { console.log('Both done') }) })
We can write it as follows:
foo() .then( res => bar() ) // bar() returns a Promise .then( res => { console.log('Both done') })
This flattens the code.
The then() method of a Promise object lets us do a task after a Promise has been fulfilled or rejected. The task can also be another event-driven or callback-based asynchronous operation.
The then() method of a Promise object takes two arguments, that is, the onFulfilled and onRejected callbacks. The onFulfilled callback is executed if the Promise object was fulfilled, and the onRejected callback is executed if the Promise was rejected.
The onRejected callback is also executed if an exception is thrown in the scope of the executor. Therefore, it behaves like an exception handler, that is, it catches the exceptions.
The onFulfilled callback takes a parameter, that is, the fulfilment value of the promise. Similarly, the onRejected callback takes a parameter, that is, the reason for rejection:
ajaxCallPromise('http://example.com/page1').then( successData => { console.log('Request was successful') }, failData => { console.log('Request failed' + failData) } )
When we reject the promise inside the ajaxCallPromise definition, the second function will execute (failData one) instead of the first function.
Let's take one more example by converting setTimeout() from a callback to a promise. This is how setTimeout() looks:
setTimeout( () => { // code here executes after TIME_DURATION milliseconds }, TIME_DURATION)
A promised version will look something like the following:
const PsetTimeout = duration => {
return new Promise((resolve, reject) => {
setTimeout( () => {
resolve()
}, duration);
})
}
// usage:
PsetTimeout(1000)
.then(() => {
console.log('Executes after a second')
})
Here we resolved the promise without a value. If you do that, it gets resolved with a value equal to undefined.
The catch() method of a Promise object is used instead of the then() method when we use the then() method only to handle errors and exceptions. There is nothing special about how the catch() method works. It's just that it makes the code much easier to read, as the word catch makes it more meaningful.
The catch() method just takes one argument, that is, the onRejected callback. The onRejected callback of the catch() method is invoked in the same way as the onRejected callback of the then() method.
The catch() method always returns a promise. Here is how a new Promise object is returned by the catch() method:
To understand the catch() method, consider this code:
ajaxPromiseCall('http://invalidURL.com') .then(success => { console.log(success) }, failed => { console.log(failed) });
This code can be rewritten in this way using the catch() method:
ajaxPromiseCall('http://invalidURL.com') .then(success => console.log(success)) .catch(failed => console.log(failed));
These two code snippets work more or less in the same way.
The resolve() method of the Promise object takes a value and returns a Promise object that resolves the passed value. The resolve() method is basically used to convert a value to a Promise object. It is useful when you find yourself with a value that may or may not be a Promise, but you want to use it as a Promise. For example, jQuery promises have different interfaces from ES6 promises. Therefore, you can use the resolve() method to convert jQuery promises into ES6 promises.
Here is an example that demonstrates how to use the resolve() method:
const p1 = Promise.resolve(4);
p1.then(function(value){
console.log(value);
}); //passed a promise object
Promise.resolve(p1).then(function(value){
console.log(value);
});
Promise.resolve({name: "Eden"})
.then(function(value){
console.log(value.name);
});
The output is as follows:
4 4 Eden
The reject() method of the Promise object takes a value and returns a rejected Promise object with the passed value as the reason. Unlike the Promise.resolve() method, the reject() method is used for debugging purposes and not for converting values into promises.
Here is an example that demonstrates how to use the reject() method:
const p1 = Promise.reject(4); p1.then(null, function(value){ console.log(value); }); Promise.reject({name: "Eden"}) .then(null, function(value){ console.log(value.name); });
The output is as follows:
4 Eden
The all() method of the Promise object takes an iterable object as an argument and returns a Promise that fulfills when all of the promises in the iterable object have been fulfilled.
This can be useful when we want to execute a task after some asynchronous operations have finished. Here is a code example that demonstrates how to use the Promise.all() method:
const p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 1000);
});
const p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 2000);
});
const arr = [p1, p2];
Promise.all(arr).then(function(){
console.log("Done"); //"Done" is logged after 2 seconds
});
If the iterable object contains a value that is not a Promise object, then it's converted to the Promise object using the Promise.resolve() method.
If any of the passed promises get rejected, then the Promise.all() method immediately returns a new rejected Promise for the same reason as the rejected passed Promise. Here is an example to demonstrate this:
const p1 = new Promise(function(resolve, reject){
setTimeout(function(){
reject("Error");
}, 1000);
});
const p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve();
}, 2000);
});
const arr = [p1, p2];
Promise.all(arr).then(null, function(reason){
console.log(reason); //"Error" is logged after 1 second
});
The race() method of the Promise object takes an iterable object as the argument and returns a Promise that fulfills or rejects as soon as one of the promises in the iterable object is fulfilled or rejected, with the fulfillment value or reason from that Promise.
As the name suggests, the race() method is used to race between promises and see which one finishes first. Here is a code example that shows how to use the race() method:
var p1 = new Promise(function(resolve, reject){ setTimeout(function(){ resolve("Fulfillment Value 1"); }, 1000); }); var p2 = new Promise(function(resolve, reject){ setTimeout(function(){ resolve("fulfillment Value 2"); }, 2000); }); var arr = [p1, p2]; Promise.race(arr).then(function(value){ console.log(value); //Output "Fulfillment value 1" }, function(reason){ console.log(reason); });
Now at this point, I assume you have a basic understanding of how promises work, what they are, and how to convert a callback-like API into a promised API. Let's take a look at async/await, the future of asynchronous programming.
If you found this article useful, do check out the book Learn ECMAScript, Second Edition for learning the ECMAScript standards to design your web applications.