Introduction
JavaScript is the language of the browser. It was designed to manipulate the DOM, throw requests to servers etc. However, we all are smitten with JavaScript because of its nature of being asynchronous and being non-blocking. This liberty of asynchronous programming does at a price though. These powerful tools have made our life easy and hard at the same time. This article will focus on pitfalls of asynchronous programming and its mitigation.
If we were to focus on the way data is retrieved from the server, much has changed. We could communicate with the server:
If we take the XMLHttpRequest object, it used to allow us to raise requests in both the above mentioned forms.
var _xhr = new XMLHttpRequest();
_xhr.onload = function(e){
if(_xhr.readyState == 4){
if(_xhr.status == 200){
// success
}
else{
// error
}
}
}
_xhr.onerror = function(error){
// request error
}
_xhr.open(_method, url, _isAsynchronous);
// _isAsynchronous set to "true" used to raise requests asynchronously.
// Note that SYNCHRONOUS requests now are deprecated in the main
// application thread.
_xhr.send();
Anyway, now that synchronous communication is out (well, rare use cases go for synchronous requests), we will focus only on the asynchronous category. Say that our application makes quite a number of XHRs to the server such that result of nth call depends on the data received from (n-1)th call. So, if I could use jQuery, this would look like:
$.get(url, function(res1){
$.get(url2, function(res2){
$.post(url3, function(res3){
// process response here
});
});
});
In this contrived example, we're trying to carry out a series of asynchronous operations but in a rather synchronous fashion. We see that the level of nesting is quite irksome and that maintaining such a code base is any developer's nightmare. This phenomena is called Callback Hell where we've callbacks within callbacks and so on. Rather, we could do away with so much clutter and make this code quite flat.
Promises
As the word says, it's a promise (technically an object) which represents the pending result of an asynchronous operation. It's not the result itself; whose value is to be yet determined in the near future. Promise objects just act as proxies to the actual data which would be available once the async operation is complete. This design paradigm allows us to forgo callback hell and write flatter and cleaner code. There exist a myriad of external libraries implementing Promises. However, as they are now available natively, so we will focus on the native implementation.
A promise could be in the following states:
To check if your browser supports Promises, you could do:
if(window.Promise){
// Promises are supported
}
As well as,
if(typeof Promise !== "undefined"){
// Promises are supported
}
Creating and Using Promises
A Promise could be constructed as:
var promise = new Promise(function(resolve, reject){
// all asynchronous operations go here
if(all is well)
resolve("hurray");
else reject("something went wrong");
});
The Promise constructor takes one argument, a function which takes two arguments. These two arguments are callbacks which would be executed when the promise is settled (either fulfilled or rejected).
A promise if fulfilled i.e. the action associated with them is successful is then resolved using the resolve callback and if it's rejected i.e the action is a failure then it is rejected using the reject callback. The Promise interface implements two major methods (source: MDN):
Since the promise is the result of a pending asynchronous operation, how do we realize if it has been settled? We use the then() method to schedule further actions to the promise once it has either been fulfilled or rejected. Promises have been designed such that its instant state and settlement value cannot be revealed without calling the then() method. The promise status, values upon fulfillment or rejection, are code inaccessible. However they are inspectable for debugging purposes. The Promise create above could be used as:
promise.then(function(res){
console.log(res); // hurray
}, function(res){
console.log(res); // something went wrong
});
The promise created either resolves or rejects when the asynchronous operation completes using the then method to apply the corresponding event handlers. We will go through the catch method in a while.
Lets try an example. We will try to fetch some awesome tweets from a server.
function fetchTweets(){
//this is an asynchronous operation. So we will return a Promise and then try
// to resolve/reject it later
var promise = new Promise(function(resolve, reject){
var _xhr = new XMLHttpRequest();
_xhr.open("GET", tweetURL);
_xhr.onload = function(){
if(_xhr.readyState == 4){
if(_xhr.status == 200){
//all is well. Our action is successfully completed.
// We should resolve our Promise.
resolve(_xhr.response);
}
else{
//not so good. Something went wrong somewhere.
// We will reject the Promise made.
var error = new Error("something went wrong");
reject(error);
}
}
}
_xhr.onerror = function(error){
reject(error);
}
_xhr.send();
});
return promise;
}
The function fetchTweets when called returns a Promise (not some data response) which later at some point in time will be resolved or rejected depending on whether the action associated with the Promise was a success or failure.
fetchTweets()
.then(function(response){
//we have the tweets here. Proceed further.
}, function(error){
//something went wrong and the Promise was rejected.
});
This coding paradigm could be translated as fetchTweets first, then if all is well proceed further, else handle the error. Cool right ! We've achieved a very flat (apparently synchronous) way of programming an asynchronous operation.
Solving Callback Hell
Asynchronous programming has been deemed to be the strength of JavaScript over other heavy-weight languages. But as it turns out, async programming is rather the Achilles heel of JavaScript. This gives way to considerable issues of which the prominent one is Callback Hell. Promises help bypass this problem using a concept of chaining promises. We could chain a set of Promises together such that the outcome of one Promise depends on the resolution of the previous Promise.
Lets expand on our tweets example. So we fetched tweets and then lets say we want to post a tweet thereafter.
function postTweet(myTweet){
return new Promise(function(resolve, reject){
var _xhr = new XMLHttpRequest();
_xhr.open("POST", tweetURL);
_xhr.onload = function(){
if(_xhr.readyState == 4){
if(_xhr.status == 200){
//all is well. Our action is successfully completed.
// We should resolve our Promise.
resolve(_xhr.response);
}
else{
//not so good. Something went wrong somewhere.
// We will reject the Promise made.
var error = new Error("something went wrong");
reject(error);
}
}
}
_xhr.onerror = function(error){
reject(error);
}
_xhr.send("tweet="+myTweet);
});
}
So, now lets combine or chain these two asynchronous operations.
fetchTweets()
.then(function(tweets){
return postTweet("this is a tweet");
// returns a Promise which hence could be resolved/rejected
})
.then(function(response){
console.log(response);
});
We've chained two asynchronous operations, the first operation fetchTweets returns a Promise which when fulfilled calls postTweet which again returns a Promise. We could go on and chain any number of asynchronous operations. This ensures that we're free from the cluttered code, and hence results in better, flat and easy-to-maintain code.
Error handling in Promises is pretty straight forward. All Promise rejections move to the next then with a rejection callback. So, if fetchTweets rejects, then it would move to the next then with a reject callback.
fetchTweets()
.then(function(tweets){
return postTweet("this is a tweet");
//this returns a Promise which hence could be resolved/rejected
})
.then(function(response){
console.log(response);
},
function(error){
//reject callback
});
Further, we could also use catch blocks to react to situations where a Promise is rejected. For example:
someAsyncOperation()
.then(function(){
return anotherAsyncOperation();
})
.then(function(){
return yetAnotherAsyncOpertaion();
})
.catch(function(){
return failedAsyncOperation();
})
.then(function(){
return someOtherAsyncOpertaion();
});
Fig.1 : Process flow using "catch" blocks
So, the flow is like this: someAsyncOperation() when succeeds will go onto the next then where anotherAsyncOpertaion() returns another Promise. However, if either someAsyncOperation() or anotherAsyncOpertaion() or yetAnotherAsyncOpertaion() fail, they would be caught in the catch block which raises failedAsyncOperation(). The logic flow in such cases is that catch blocks proceed to the next then statements. So, after failedAsyncOperation() returns a Promise, the following then is executed wherein if successful, someOtherAsyncOperation() will be called which would return a Promise.
Promise.all API
If we have a situation wherein a lot of Promises need to be executed and only then we could proceed, then the Promise interface implements a static method, Promise.all which is the thing we need. So, if we have a bunch of read-tweets and write-tweets operations (each returning a Promise) then we could create an array of all Promises and do something like:
Promise.all([promiseOne, promiseTwo]).then(function(response){
// good to go. All Promises have been successfully fulfilled.
var res1 = response[0];
var res2 = response[1];
}, function(error){
// one or more Promises have failed
});
The success callback is however called ONLY if all the Promises have been fulfilled. The response in the success handler will be an array containing the results of each of the promises. The order of responses in the array will be the same as they were passed to the all() method. If one of them get rejected, then entire operation is rejected with the reject handler handling the first failed promise and the remaining promises remain unfulfilled. Using this method gives a performance edge compared to executing the Promises in a sequence.
Promise and jQuery
If your application uses jQuery, then you might have used the $.ajax() API. This API is actually a superset of the standard XMLHttpRequest API. The point here is that objects returned by $.ajax(), jQuery XHR objects (jqXHR), actually implement the Promise interface. This allows to assign multiple callbacks to a single request as we do in Promises. For e.g.:
var jqXHR = $.ajax({
url : "myServer.php",
statusCode: function(){
500: function(){
alert("internal server error !");
}
}
});
jqXHR.done(function(data, textStatus, jqXHR){
// success callback
});
jqXHR.fail(function(jqXHR, textStatus, errorThrown){
// error callback
});
jqXHR.always(function(data|jqXHR, textStatus, jqXHR|errorThrown){
// complete callback. The parameters to this function will be
// contingent on whether the request succeeded/failed
});
Non-native support
The concept of Promise is not novel at all. There have been considerable number of popular implementations already:
Browser Support
Promises are supported in relatively newer versions of Chrome, Firefox and Opera. Mobile browsers, apart from Chrome for Android and FF Mobile, do not support them. More details could be found here.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.