Thursday, May 24, 2012

jQuery Deferred Objects Promise Callbacks

jQuery Deferred/Promise objects untangle spaghetti-like asynchronous callbacks.


Introduced in jQuery 1.5, Deferred/Promise type is an implementation of CommonJS Promise/A interface. The new types change the way asynchronous methods are called. For example, $.ajax() now returns a Promise object, to which you can attach callbacks. Prior to version 1.5, you make ajax calls like this,
Since version 1.5, you can rewrite the above code like this,
So what's the big deal? Isn't it just another syntax sugar for consistency with jQuery's fluent style? No, it is not a simple syntax change. If it were just for the fluent syntax, it should have been introduced since version 1.0. It is a paradigm shift in asynchronous programming.

I am not going to repeat or rephrase the definition of Deferred/Promise, nor give syntactic usage examples out of context. You can get them from jQuery website, Deferred/Promise. If you are still in the dark after reading the official documentation, then try the following explanation within the context of asynchronous code examples. More specifically, I will show you how asynchronous callbacks make a simple for loop and an if…else branch like spaghetti, and how Deferred/Promise objects untangle the spaghetti code.

Loop example

The example repeats 5 tasks sequentially in a loop, and reports final result after the loop. Let's first look at its synchronous implementation. Although dead simple, the example shows a clear-cut separation between the work logic and the control logic. The worker function, doTask(), does not know or care anything about how it is called; while the main function focuses on execution flow.

Now let's make the worker function asynchronous.
The asynchronous version of doTask() initiates a task, and returns immediately. The task will finish after a random delay. The asynchronicity distorts execution flow. The execution flow does not follow code flow any more. The final report is always executed first; and then the 5 steps are finished in random order.

In order to restore the original execution flow using old-fashioned callbacks, the code flow has to be re-structured. There are two options to re-structure the code. The first option is to use nested callbacks: Two observations for nested callbacks:
  1. The worker function is no longer independent of control logic. It needs to know what to do next. 
  2. The main function is not scalable any more. It is trivial to scale the synchronous version to 100 steps or even 1 million steps. Try that on the nested callbacks!
The other option is to use recursive callbacks: Now, the work logic and the control logic are completely mixed together. Although this version is shorter than the nested callback version, it is actually harder to understand what it does.

One way or the other, callbacks break the separation between the work and the control logic, mix them together, and make spaghetti-like code. If we add task dependency into the control logic, callbacks will make the code even more tangled, as shown in the next code example.

Branch example

In this example, the first task (doTask1) returns a random integer between 0 and 10. If the return value is greater than or equal to 5, then task 2 (addTask) is executed; otherwise, task 3 (multiplyTask) is executed. First, let's have a look at the synchronous version. Please note again the separation between the work and the control logic.
Next, let's look at the asynchronous version.
Once again, the work and the control logic are tangled all over the place. Just reading the code, are you able to figure out what it does without the hint from the synchronous version?

Asynchronous functions are so detrimental to code flow due to their inability to return execution results to their callers. When you call a synchronous function, you can say, "hey, worker, this is what you need to do. Just finish your task. Give me the result. I will take care of the rest." The ability to get hold onto return values enables you to control the execution flow, as well as the code flow. In synchronous programming, execution flow is equivalent to code flow. When you call an asynchronous function, you would have to say this. "Hey, worker, this is what you need to do. Since you don't know when the result will be back, I will have to leave some code here for you to decide what to do next. If you get this result, run this piece of code. If you get that result, run that piece of code. If you don't get any result, then run the fail code." Because you can't get hold of the return value at the end of an asynchronous call, you lose control of the execution flow. You also have to break up the code flow to match the tangled execution flow.

Now it's time to see how Deferred/Promise objects can untangle the spaghetti code.

Deferred/Promise untangle spaghetti code

First, the loop example.
Then, the branch example.
Please appreciate how Deferred/Promise objects restore the separation between the work logic and the control logic, making the code very much similar to their synchronous versions.

You can think of a Deferred/Promise object as an on-going process, which you can wrap around an asynchronous function. Now this is what you can say to the worker. "Hey, worker, this is what you need to do. I have created a process for you. Just finish your task in the process. Don't worry about anything else. I will monitor the process, and take care of the rest." The ability to wrap asynchronous code within and then manipulate a Deferred/Promise object gives you back the control of code flow.

Paradigm shift

In old-fashioned callback paradigm, asynchronous methods are void black holes. The first time you call an asynchronous method, you pass the point of no return. You lose the control of code flow, giving it up to the asynchronous method.

In the new paradigm, asynchronous methods return Deferred/Promise objects. A Deferred/Promise object is an abstraction of an ongoing process. You can monitor the status of the process, attach callbacks to be executed when the process is done or failed, or chain another process to the current process. Most importantly, an asynchronous method returns code flow to its calling method, so you are still in control.