99

When doing single-threaded asynchronous programming, there are two main techniques that I'm familiar with. The most common one is using callbacks. That means passing to the function that acts asynchronously a callback-function as a parameter. When the asynchronous operation will finish, the callback will be called.

Some typical jQuery code designed this way:

$.get('userDetails', {'name': 'joe'}, function(data) {
    $('#userAge').text(data.age);
});

However this type of code can get messy and highly nested when we want to make additional async calls one after the other when the previous one finishes.

So a second approach is using Promises. A Promise is an object that represents a value which might not yet exist. You can set callbacks on it, which will be invoked when the value is ready to be read.

The difference between Promises and the traditional callbacks approach, is that async methods now synchronously return Promise objects, which the client sets a callback on. For example, similar code using Promises in AngularJS:

$http.get('userDetails', {'name': 'joe'})
    .then(function(response) {
        $('#userAge').text(response.age);
    });

So my question is: is there actually a real difference? The difference seems to be purely syntactical.

Is there any deeper reason to use one technique over the other?

3
  • 8
    Yes: callbacks are just first-class-functions. Promises are monads that provide a composable mechanism to chain operations on values, and happen to use higher-order functions with callbacks to provide a convenient interface.
    – amon
    Commented Nov 12, 2015 at 22:29
  • 1
  • 5
    @gnat: Given the relative quality of the two questions/answers, the duplicate vote should be the other way around IMHO. Commented Nov 13, 2015 at 9:04

1 Answer 1

113

It is fair to say promises are just syntactic sugar. Everything you can do with promises you can do with callbacks. In fact, most promise implementations provide ways of converting between the two whenever you want.

The deep reason why promises are often better is that they're more composeable, which roughly means that combining multiple promises "just works", while combining multiple callbacks often doesn't. For instance, it's trivial to assign a promise to a variable and attach additional handlers to it later on, or even attach a handler to a large group of promises that gets executed only after all the promises resolve. While you can sort of emulate these things with callbacks, it takes a lot more code, is very hard to do correctly, and the end result is usually far less maintainable.

One of the biggest (and subtlest) ways promises gain their composability is by uniform handling of return values and uncaught exceptions. With callbacks, how an exception gets handled may depend entirely on which of the many nested callbacks threw it, and which of the functions taking callbacks has a try/catch in its implementation. With promises, you know that an exception which escapes one callback function will be caught and passed to the error handler you provided with .error() or .catch().

For the example you gave of a single callback versus a single promise, it's true there's no significant difference. It's when you have a zillion callbacks versus a zillion promises that the promise-based code tends to look much nicer.


Here's an attempt at some hypothetical code written with promises and then with callbacks that should be just complex enough to give you some idea what I'm talking about.

With Promises:

createViewFilePage(fileDescriptor) {
    getCurrentUser().then(function(user) {
        return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
    }).then(function(isAuthorized) {
        if(!isAuthorized) {
            throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
        }
        return Promise.all([
            loadUserFile(fileDescriptor.id),
            getFileDownloadCount(fileDescriptor.id),
            getCommentsOnFile(fileDescriptor.id),
        ]);
    }).then(function(fileData) {
        var fileContents = fileData[0];
        var fileDownloads = fileData[1];
        var fileComments = fileData[2];
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }).catch(showAndLogErrorMessage);
}

With Callbacks:

createViewFilePage(fileDescriptor) {
    setupWidgets(fileContents, fileDownloads, fileComments) {
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }

    getCurrentUser(function(error, user) {
        if(error) { showAndLogErrorMessage(error); return; }
        isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
            if(error) { showAndLogErrorMessage(error); return; }
            if(!isAuthorized) {
                throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
            }

            var fileContents, fileDownloads, fileComments;
            loadUserFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileContents = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getFileDownloadCount(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileDownloads = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getCommentsOnFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileComments = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
        });
    });
}

There might be some clever ways of reducing the code duplication in the callbacks version even without promises, but all the ones I can think of boil down to implementing something very promise-like.

4
  • 1
    Another major advantage of promises is that they're amenable to further "sugarification" with async/await or a coroutine that passes back the promised values for yielded promises. The advantage here is that you get the ability to mix in native control flow structures, which may vary in how many async operations they perform. I'll add a version that shows this.
    – acjay
    Commented Oct 25, 2016 at 18:38
  • 10
    The fundamental difference between callbacks and promises is the inversion of control. With callbacks, your API must accept a callback, but with Promises, your API must provide a promise. This is the primary difference, and it has broad implications for API design.
    – cwharris
    Commented Feb 7, 2017 at 22:24
  • 1
    @ChristopherHarris not sure I'd agree. having a then(callback) method on Promise that accepts a callback (instead of a method on API accepting this callback) doesn't have to do anything with IoC. Promise introduces one level of indirection that is useful for composition, chaining and error handling (railway oriented programming in effect), but callback is still not executed by the client, so not really absence of IoC. Commented Feb 17, 2018 at 21:38
  • 2
    @dragan.stepanovic You're right, and I used the wrong terminology. The difference is the indirection. With a callback, you must already know what needs to be done with the result. With a promise, you can decide later.
    – cwharris
    Commented Feb 17, 2018 at 23:14

Not the answer you're looking for? Browse other questions tagged or ask your own question.