In javascript, the completion of an asynchronous function is handled via call backs. We generally provide two callback functions to handle success and failure result of async function. Sometimes a situation arises where the execution of one async function depends upon the result of previous async function. The typical example of an asynchronous function is jquery’s ajax request.
Traditional approach
Consider an example where a user is uploading a file from browser to server. To upload the file, the user first fetch an access token from server, then he upload the file to server. The typical way to handle such scenario using jquery is showing in following jsfiddle :
http://jsfiddle.net/hemant24/9ost5vsw/
Now let’s say we have an additional requirement, in which the location about the file being uploaded also comes from the server. It means now we need 3 ajax calls – First, to get the access token. Second, to get the uploading file location, and then the third call, to actually upload the file. We approach this in jquery as follows :
http://jsfiddle.net/hemant24/v0L1gdq9/1/
What if now we want to do some action on the uploaded file, say POST a new task which converts the uploaded file from one format to another like from an epub to a pdf ?
As you can see, we are chaining the ajax requests, which has poor readability and becomes difficult to handle.
The trend these days is to offload the workload from the server to client, making clients more like desktop UI. Such chained calls are becoming increasingly common – we faced such scenarios in a couple of projects recently. Extending the above example, let’s assume we have a use case which involves in dealing with format conversion of an uploaded file.
We have the following ajax calls :
- Get an access token ( either from server or from cookies ) - Get an upload location - Upload a file - Post a task - Fetch task's status until It get changed to 'success' - Display converted file to html page
To handle such scenarios make some space for jquery’s Deferred and Promise in your javascript tool box.
Introduction to $.Deferred
Deferred is created through $.Deferred() method. Each deferred object will have a state associated to it. The state could be ‘pending’, ‘resolved’ or ‘rejected’. You can control the state of deferred object by using ‘resolved()’ or ‘rejected()’ functions. In the following code, when you first create the $.Deferred object, it will be in ‘pending’ state. After calling deferred.resolved() the state will change from ‘pending’ to ‘resolved’. Once you marked the deferred object either ‘resolved’ or ‘rejected’, you cannot change it to other state. Open the following jsfiddle snippet to see code in action:
http://jsfiddle.net/hemant24/2hjL8xmj/3/
The true power of deferred lies in callback function. You can add callback function to deferred object through ‘done()’, ‘fail()’, ‘then()’ or ‘always()’ functions. Whenever state of deferred object changes from ‘pending’ to ‘resolved’, callback function attached through ‘done()’ will be called and ‘fail()’ callback function is called when deferred object’s state change from ‘pending’ to ‘rejected’. ‘always()’ callback function is called in both the cases. ‘then()’ can take both success and error callback function.
Consider the following fiddle:
http://jsfiddle.net/hemant24/pkrzwwzz/1/
Here, multiple callback functions are called in the same order in which they were added.
Introduction to $.Promise
A Promise is similar to a deferred, but it is ‘Read Only State’. It means you can not change the state of promise object. Only the owner of promise can change its state. Here owner is the deferred object. A Promise is created through deferred object, as shown in following code.
var deferred = new $.Deferred() var promise = deferred.promise()
To change the state of promise, call either deferred.resolved() or deferred.rejected(). Promise is mainly used to add callback function. And other use case is to give promise object to those functions that are not supposed to change the state of promise .
The following fiddle illustrates a promise in action:
http://jsfiddle.net/hemant24/yqhbe2zt/1/
Combining Multiple Deferred
Jquery introduced $.Deferred/$.Promise in version 1.5. It is an implementation of commonjs/Promise A spec. A lots of libraries available in the market implementing the Promise A specs like ‘q’. From 1.5 version onward, all jquery’s ajax request return promise object.
Deferred makes dealing with asynchronous function much more pleasant and readable. $.Deferred or $.Promise can be combined together to manage complicated workflows relatively easily. One can combine them to work in parallel or in sequence.
Sequencing Pattern
Earlier, we discussed the issues with ajax chaining. We can solve this problem elegantly by using $.Deferred and $.Promise.
The pseudocode below illustrates the code flow. In the first fragment, we have created some functions which return promise object. In the second fragment section we make use of them to create a sequential workflow.
// Function to get access token either from server or from previously saved cookies. var getAccessToken = function(){ if(isLoggedIn) { var deferred = $.Deferred(); var session = JSON.parse($.cookie('test.session')); deferred.resolve({access_token: session.key}); return deferred.promise(); }else{ return $.ajax({ type: "post", url: "http://test.com/v1/oauth/token/", data : { client_id : client_id, client_secret : client_secret, grant_type : 'client_credential' }, dataType: "json" }) } } // Function to get location to upload file var getUploadLocation = function(accessToken) { return $.ajax({ type: "get", url: "http://test.com/v1/file/upload/location/?q=nearest", data: { access_token:accessToken }, dataType: "json" }) } // Function to upload file. var uploadFile = function(url) { var deferred = $.Deferred(); fileUtils.uploadFile( url, function(success, data) { if(success) { return deferred.resolve(data); } else { return deferred.reject('Upload Failed'); }; return deferred.promise(); }; var postTask = function(taskRequest) { var apiTaskRequest = createTaskRequestPayload(); return $.ajax({ type: "post", contentType : 'application/json', url: "http://test.com/v1/task/", data : JSON.stringify(apiTaskRequest ), dataType: "json" }); }; var errorHandler = function(error){ var errorMsg = parseProperErrorMsg(error); alert(errorMsg); }
In following code we have used the above functions to create the required work flow. Notice that we have provided error call back only to the last ‘then()’ in the chain. This is because the way chaining works, if some error occurred midway through the execution, jquery searches down all chain elements until it finds the first error callback handler.
getAccessToken() .then(getUploadLocation) .then(uploadFile) .then(postTask, errorHandler)
The readability of the above code has improved quite a bit compared to the chaining example earlier. In addition to readability it is also very flexible. You can use the same functions to create different work flow all together. Let say we already have the URL of file which needs to be converted. In this case we don’t want to upload the file. We can reuse the functions and create following flow.
getAccessToken() .then(prepareTaskPostRequest) .then(postTask, errorHandler)
Parallel Pattern
In some situations, you may want to do some action only after the completion of multiple asynchronous task. Let’s take an example : We want to calculate a user’s remaining conversion volume in current subscription. Volume information is stored in two different resources, one is subscription resource and the other is statistics resource. Subscription resource returns volume in current subscription. Statistics resource will hold the information about the usage of conversion done so far. To handle such cases we use $.when. $.when is equivalent to the logical AND operator. Given a list of promises, $.when() return new $.promise() that obey following rule.
- When all of the given promises are resolved, the new Promise is resolved.
- When any of the give promises is rejected, the new Promise is rejected.
var getUserSubscription = $.get('/subscription') var getUserStats = $.get('/stats') $.when( getUserSubscription , getUserStats ).then(function(subscription, stats){ var totalVolume = subscription['volume']; var usedVolume = stats['usage']; var remainingVolumen = totalVolume - usedVolume; })
Conclusion
In our experience, $.Deferred and $.Promise are a good to handle asynchronous functions. They help one to write more readable and maintainable code. They are a great addition to a seasoned javascripter’s toolbox.