Writing code in an async programming framework like node.js becomes super complex and unbearably ugly, superfast. You are dealing with synchronous and asynchronous functions, and mixing them together can get really tricky. That is why we have Control Flow.
From Wikipedia,
In computer science, control flow (or alternatively, flow of control) refers to the order in which the individual statements, instructions, or function calls of an imperative or a declarative program are executed or evaluated.
Basically, control flow libraries allow you to write non-blocking code that executes in an asynchronous fashion without writing too much glue or plumbing code. There is a whole section on control-flow modules to help you unwind the spaghetti of callbacks that you will quickly land yourself into while dealing with async calls in node.js.
Control flow as a pattern can be implemented via several approaches, and one of such approaches is called Promises.
First things first, Promises is not a new language feature of JavaScript. Nothing has fundamentally changed in the language itself to support Promises. They could have been easily implemented 5 years back as well (and probably were). Promises is a pattern to solve the traditional callback hell problem in the async land. There are other similar approaches to solve the same problem as well.
The reason people are talking about Promises and other similar patterns now is partly because of two reasons, in my opinion:
- Five years back, the web was much simpler. The amount of complex, JavaScript based, front-end heavy web apps dealing with several asynchronous calls simultaneously were few and far between. In many ways, there was no general need for a pattern like Promises.
- The second reason in my opinion is node.js. Thanks to node.js, people are now writing all the backend code in async fashion, backend code where all the heavy lifting is done to generate the right response from a multitude of data sources. All these data sources are queried in async fashion in node.js, and there is an urgent need of control flow to manage concurrency.
Because of the above two reasons, a lot of frameworks have started providing the concept of Promises out of the box. CommonJS has realized that the pattern needs to be standardized across these various frameworks and has a spec now, http://wiki.commonjs.org/wiki/Promises
OK, so now that it’s clear that Promises is a pattern, what exactly is it, and how is it different from other traditional approaches?
From Wikipedia,
In computer science, future, promise, and delay refer to constructs used for synchronization in some concurrent programming languages. They describe an object that acts as a proxy for a result that is initially not known, usually because the computation of its value has not yet completed.
Yep, that’s nice in theory, but in practice, this is what changes:
asyncCall(data, function (response) {
// You have a response from your asynchronous call here
}, function (err) {
// If the async call fails, the error callback is invoked
});
And now with Promises pattern, you would write it like this:
let promise = asyncCall(data);
promise.done(function (response) {
// You have response from your asynchronous call here
}).fail (function(err) {
// If the async call fails, you have the error response here.
});
Looks almost the same thing. In fact, it looks like Promises can hide the true nature of a function. With Promises, it can be super hard to tell if a call is synchronous or asynchronous. I say so because this is how your JSDoc for your async call function would change.
/**
* asynchronousCall function
*
* @param data - input data
* @param success callback. Should accept `response` parameter
* @param failure callback. Should accept `error` parameter
* @return void
*/
to
/**
* asynchronousCall function
*
* @param data - input data
* @return promise object- It will resolve with `response` data
* or fail with `error` object
*/
Some people will argue the second style is cleaner. I am more in favour of the first style on this one.
If you are a small, scrappy startup with a fast-growing codebase, it’s really hard to have proper documentation. In such scenarios where proper documentation is missing, Promises are a dealbreaker for me as more than once, you will have to look within a function just to understand how to consume it and whether it’s asynchronous or not. With the first style, the function definition is very explicit and usage doesn’t rely heavily on the documentation. If there is a callback, it most likely is asynchronous.
Having said that, there are some pros in the Promises approach.
The first one that is usually talked about is how Promises are good for dealing with a multitude of async/sync calls. Taming multiple async calls becomes much easier with Promises.
Imagine a typical scenario where one has to perform something only after a list of tasks are done. These tasks can be both asynchronous or synchronous in nature in themselves. With Promises, one can write code that functions identically calling these synchronous and asynchronous functions, without getting into the nested callback hell that might otherwise happen.
let fn = function () {
let dfd = new Deferred();
let promises = [];
promises.push(async1()); // an async operation
promises.push(async2()); // an async operation
promises.push(sync1()); // a sync operation
promises.push(async3()); // an async operation
// You want to perform some computation after above 4 tasks are done
Promise.when(promises).done(function () {
for (let i = 0, len = arguments.length; i < len; i++) {
if (!arguments[i]) {
dfd.resolve(false);
}
}
dfd.resolve(true);
}).fail(function (err) {
dfd.reject(err);
});
return dfd.promise();
};
This function returns a promise, based on the results of a bunch of tasks. These tasks are a mix of async and sync calls. Notice how, in the code, It does not have to distinguish between those calls. This is a huge benefit to most of the Promises implementations.
Typically, all Control-flow libraries following other patterns also provide this benefit, though. An example is async module in node.js modules. After all, this is the entire reason behind control-flow. The difference between Promises based implementations vs callback based implementations is subtle and more a matter of choice than anything else.
Another advantage that is given to Promises based implementations is how they are helpful in attaching multiple callbacks for an async operation. With Promises, you can attach listeners to an async operation via done
even after the async call has completed. If attached after completion, that callback is immediately invoked with the response.
Traditionally, we have been solving this problem using Events. An event is dispatched when an async task completes and all those who care, can subscribe to the event prior and execute on it when the event happens. The typical issue with Events though is that you have to subscribe early, else you can miss an event.
let t1 = Event.subscribe('asyncOp', function (data) {
// Listener for the data that comes in when the event happens
});
async(data, function (response) {
Event.dispatch('asyncOp', response);
});
let t2 = Event.subscribe('asyncOp', function (data) {
// Listener for the data that comes in when the event happens
// This might never fire if we subscribe after the event already fired
});
With Promises, you can attach those callbacks at any point of time without caring if the call is already complete or not.
let p = async(data);
p.done(function (response) {
// Do task 1
});
p.done(function (response) {
// Do task 2
});
The second example is definitely cleaner and helps in structuring your code better. You don’t have to bother about subscribing early, too.
This is more of a clear win for the Promises based approach in my book. As I said, you can solve the same problem with an Event based approach as well, but designing an Event registry just to solve the issue of multiple callbacks would be an overkill. Events by nature bring more to the table like propagation, default behaviour etc. If your app requires them for better reasons, use them by all means for multiple callbacks too. They provide a viable solution. However, if you are trying to solve just the issue of multiple callbacks, Promises do it pretty well.
To summarize my thoughts, I think Promises are a nice pattern, but nothing too special. Similar functionality can be mocked in other approaches too. However, if you decide to use a Promises implementation, make sure you are strictly documenting your code as otherwise it becomes an organizational problem to bring new developers on board with your codebase.
Reference: https://github.com/wolffe/Promises