- Concurrency
- Concurrent programming languages
- Problems with building applications with concurrency: synchronization
- Concurrent JavaScript programs in the browser
In this post, we'll examine the scenarios in which synchronization through callbacks in JavaScript becomes undesirable and how promises solves our problems.
Lets look at some cases.
In the case of a program with a single asynchronous operation:
var async = function () {
setTimeout(function() {
console.log("Done!");
}, 1000);
};
async();
In this case, our program will run to completion and after about a second we'll get the log from our asynchronous operation which in this case is a timeout. This isn't so difficult to understand. We can replace the timeout function with any other asynchronous function and the code will still be clear. Moreover, we can have dozens of these calls in our program and it would still be easy to understand if they do not interact with each other. In practice, however, async operations tend not to occur in isolation.
Lets take it up a notch and look at three interacting asynchronous operations. In this example, we have operations A, B, and C. They must execute in that order.
// Make cup noodles
function op_a (cb) {
console.log("Open the cup");
cb();
};
function op_b (cb) {
console.log("Pour hot water into the cup");
cb();
};
function op_c (cb) {
console.log("Close the cup");
cb();
};
// Asynchronous Execution
// Run op_a
setTimeout(function () {
op_a(function () {
// Run op_b
setTimeout(function () {
op_b(function () {
// Run op_c
setTimeout(function () {
op_c();
}, 2000)
})
}, 4000);
})
}, 1000);
This style of using callbacks to define a series of operations is known as the continuous passing style. In a continuous passing style of programming, you are being explicit about what operations needs to happen next. Essentially, you are saying "Take this and call it when you're done". This is common in event driven programming and stands in contrast to the direct style which is the style used in most programming languages where the order of execution is implicit based on the order in which operations are defined.
As you've probably guessed, the issue here is style. It's ugly. If we add more asynchronous operations into this chain, this code will become harder to understand and modify. We'll start to descend deeper into what is commonly referred to as callback hell or the pyramid of doom. One argument in favor of this style is that the operations are defined in a linear fashion. The order of execution is clear - we just have to figure out where the individual functions begin and end. Unfortunately, it's not clear at all where functions begin or end because the deep nesting makes the code very difficult to read. Some argue that this is a superficial problem because it's a just style problem, but many would contend that style is a matter of great importance because more man hours are spent maintaining and extending code than writing it.
"First, we want to establish the idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute" - Structures and Interpretations of Computer Programs (SICP)
This pyramid of hell can be flattened to a much more readable pyramid through the use of named functions. We're still using the continuous passing style - it's just that by using names we have a much more readable nested structure. However, it's still a nested structure that will always grow in proportion to the number of operations involved. Moreover, in both cases, no matter how flat we get our pyramid to be, using CPS means we're always stuck with the issue of polluting our function namespaces with callback parameters.
Promises to the rescue
One possible "solution" to this is to avoid the problem of synchronization altogether and just make every operation invoked by JavaScript synchronous. Of course, that would be silly. Even if this were an option through some flag in the windows API, it would render JavaScript programs useless and unproductive because any long standing concurrent operation such as a network request will block the rest of the program from running - which can't possibly work in a concurrent system like the browser.
A more sound solution is to invent a new abstraction for synchronizing async operations such that we keep asynchronous I/O while being able to write interacting async functions in a more direct style.
Promises is that solution.
As I mentioned in the first post, the concept of a promise is not new. The precise technical specifications of promises in 1976 for programming languages such as E or Joule may not match the specification of promises as laid out in ES6 Harmony. However, the general function of a promise as a synchronization mechanism is the same across all implementations throughout history.
Promises were invented for concurrent systems with asynchronous operations and one of their main features is that they decouple an asynchronous operation from its result. This ability to separate the async operation from the eventual result of the computation is central to why promises are especially useful in JavaScript.
To understand why this decoupling is useful, lets revisit the purpose of the callback in JavaScript. The callback is essentially the event handler that gets pushed into our message queue which gets invoked by the JavaScript engine with the result of the operation. The reason we enter the pyramid hell is that the result of the operation never gets returned by the function! Since the result is always tied (coupled) to the asynchronous operation via its callback function, the only way forward is through nesting because we rely on each callback to push the next operation into the message queue. This is what causes the ugly outward growth. This isn't an issue in synchronous operations because the result is always returned.
Since we're dealing with asynchronous functions, what in the world can we return from the function if we don't know when it can return a result? Promises solve this problem by having async functions return an object that merely represents the eventual result of the operation.
Since we're dealing with asynchronous functions, what in the world can we return from the function if we don't know when it can return a result? Promises solve this problem by having async functions return an object that merely represents the eventual result of the operation.
var async = function () {
var promise = new myPromise();
setTimeout(function (result) {
promise.fufill(result);
}, 500);
return promise;
};
var asyncPromise = async();
This is powerful because it shifts us from thinking in terms of dealing with the results of an asynchronous function that is only accessible via a callback function to dealing with an immediately returned entity that represents the eventual result. This enables us to write asynchronous code in a more synchronous fashion (direct style).