93

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?

110

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.

  • 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 Oct 25 '16 at 18:38
  • 9
    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 Feb 7 '17 at 22:24
  • @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. – dragan.stepanovic Feb 17 '18 at 21:38
  • 1
    @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 Feb 17 '18 at 23:14

protected by gnat Mar 31 '18 at 12:06

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

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