Demystifying Promises vs Callbacks

Aren’t Promises just regular Callbacks? What are the pros/cons of using one over the other? Promises vs Callbacks? Questions like these bothering you? Not anymore! Before we actually go dive in deeper, lets quickly remind ourselves the basic definitions of these terms and what they are meant to accomplish.

Callbacks:

A Callback is a any function which is passed as a parameter to another function and is invoked/executed only after after some kind of event occurs. As a quick example, lets assume you need to open a file and write to it, you can accomplish this in 2 ways,

Without Callbacks:

fileObject = open(file);
// WAIT for the file to open, then write to it
fileObject.write("Digital Fortress");
// continue execution

Here, we need to WAIT for the file to open, before we can write to it. This “blocks” the flow of execution, and our program needs to wait before continuing further execution!

With Callbacks, we could do this instead:

// define a callback function
function writeToFile (fObj) {
    fObj.write("Digital Fortress");
}
// we pass the the CALLBACK FUNCTION 'writeToFile' as a parameter to function open()
fileObject = open(file, writeToFile(fileObject));
// execution continues i.e. we don't wait for the file to be opened

As you can see, once the file is opened, the callback function is invoked (which does the write operation), but while we wait for it to be opened, our program can do other things and it does not remain blocked!

Promises:

A Promise is an object that represents an operation which has produced or will eventually produce a value. It has 3 states:
Pending: The operation is not yet complete (Promise is pending fulfillment)
Fulfilled: The operation has finished, and the promise is fulfilled with a value. (Analogous to returning a value from a synchronous function)
Rejected: An error has occurred during the operation, and the promise is rejected with a reason. (Analogous to throwing an error in a synchronous function)

A promise is said to be settled (or resolved) when it is either fulfilled or rejected. Once a promise is settled, it becomes immutable, and its state cannot change. The then() and catch() methods of a promise can be used to attach callbacks that execute when it is settled. These callbacks are invoked with the fulfillment value and rejection reason, respectively.

As an example,

const promise = new Promise(function (resolve, reject) {
    // Perform some work (possibly asynchronous, like an http request)

    if (/* Work finished successfully and produced "value" */) {
        resolve(value);
    } else {
        // Something went wrong because of "reason"
        let reason = new Error(message);
        reject(reason);
        // Throwing an error also rejects the promise.
        throw reason;
    }
});

The then() and catch() methods can be used to attach fulfillment and rejection callbacks:

promise.then(function(value) {
    // Work completed successfully,
    // promise has been fulfilled with "value"
}).catch(function(reason) {
    // Something went wrong,
    // promise has been rejected with "reason"
});

Alternatively, both callbacks can be attached in a single call to then():

promise.then(onFulfilled, onRejected);

Check out this Fiddle for a working example.

Attaching callbacks to a promise that has already been settled will immediately place them in the microtask queue, and they will be invoked ASAP! (i.e. immediately after the currently executing script). Also, it is not necessary to check the state of the promise before attaching callbacks, unlike with many other event-emitting implementations.

Promises vs Callbacks

To answer the opening statement of whether or not promises are regular callbacks, No, Promises are not callbacks. They are more like wrappers on callbacks that provide mighty abstraction. That being said, there is nothing that Promises can do and that callbacks cannot! Although, since promises are little more than just regular callbacks,  lets enlist a few of those differences:

1. Readability

// Regular Callbacks
api1(function(result1){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

// Using Promises
api1().then(function(result1){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

With Callbacks, more the number of callbacks you chain, more difficult does it get to read and more difficult to debug as well.

Promises, on the other hand, offer much better readability allowing you to chain as many calls as you’d like. With the advent of ES6, flattening can also be easily achieved like this:

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3));

2. Error Handling

// Regular Callbacks
api1(function(error1, result2){
    if (error1) {
      // log error
    } else {
        api2(function(error2, result2){
            if (error2) {
            // log error
            } else {
                api3(function(error, result3){
                    if (error2) {
                        // log error
                    } else {
                        // do work
                    }
                });
            }
        });
    }   
});

// Using Promises
api1().then(function(result1){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

As you can see above, the solution with promises is pretty much like a try {} catch block but with regular callbacks its becomes a literal callback hell.

Advantages of Callbacks

  • Simplicity

Callbacks are simple to understand and easy to create.

  • Slightly Better Performance

Somewhat more efficient than Promises because fewer objects are created and garbage collected. Here is a JS perf test showing the performance difference between callbacks and promises on various browsers. The difference is quite negligible but worth mentioning.

Advantages of Promises

  • Parallel Execution

With Promises, you can make simultaneous calls to the 3 apis and wait for them to be resolved. An intriguing use-case could be to make ajax calls in parallel instead of waiting for each one.

Promise.all([api1(), api2(), api3()]).then(function(result) {
    // Do some work. 
    // result is an array containing values of the three fulfilled promises.
}).catch(function(error) {
    // Handle error
    // Executes when at least one of the promises was rejected.
});

Parallel Execution is not natively possible with callbacks but can be integrated using async.parallel().

  • Integrated Error Handling

Promises come with integrated error handling. The result of the computation might be that either the promise is fulfilled with a value, or it is rejected with a reason.

promise.then(function(result){
    // do something
}).catch(function(error) {
    //handle any error that may occur before this point
}).then(function() {
    // do something whether there was an error or not
    // eg: hiding a spinner while performing an AJAX request
});

Errors are handled and propagated automatically in promise chains, so that you don’t need to care about it explicitly compared to plain callbacks.

Summary

As you can see all the obvious advantages of using Promises, I would recommend all serious Javascript developers to migrate towards promises if not already 😉