Use Promises to Remove Dependencies and Increase Flexibility

Quickly: What’s a Promise?

A Promise is an object that helps you deal with asynchronous code. A lot of async code looks like this:

fetchResourceFromServer(function(resource) {
    processResource(resource, function(successCode) {
        alertUserOfSuccess();
    });
});

Promises DRY that up by encapsulating the “when this is eventually done, do this other thing” concept. Promises have a then method that does the waiting and forwarding so you don’t have to keep writing it yourself.

fetchResourceFromServer()
.then(processResource)
.then(alertUserOfSuccess);

We can see at a glance that this is easier to read, but there are other, more better benefits as well.

An Example Problem

I was challenged to write the bulk standard uploader for Haiku, to which we’re adding all of our SBG functionality from ActiveGrade. ¬†Basically, the problem was:

  1. upload a file
  2. display results after the file was processed by the server.

Both steps could take an arbitrary amount of time, depending on file size, processing power, etc. We need to display feedback to the user at each step.

Naive Solution

This is the kind of problem that is easy to describe but usually pretty annoying to implement. It might look something like this:

startFileUpload();
 
function checkIfFileIsUploaded() { 
    if (fileIsUploaded()) {
        displayProcessingMessage();
        checkIfProcessingIsDone();
    } else {
        setTimeout(checkIfFileIsUploaded, 1000); //check again in a second
    }
}
 
function checkIfProcessingIsDone() {
   if (processingIsDone()) {
       displayDoneMessage();
   } else {
        setTimeout(checkIfProcessingIsDone, 1000); //check again in a second
   }
}
 
checkIfFileIsUploaded();

There are a lot of problems with this code that will stop you from being an awesome developer.

  1. You have to read the definitions of those functions to understand what is happening
  2. If you want to insert a step between the two functions, you have to change BOTH functions, and link your new function to both existing functions. When your planning meeting comes up and someone suggests adding a step in between (“Can we ask them if they want to continue after the file is uploaded?”) you’re going to groan unprofessionally and say, “uh, I GUESS…”
  3. …and many, many more!

A Better Solution with Promises

Here’s how this code looks with promises.

    startFileUpload()
    .then(displayProcessingMessage)
    .then(waitForProcessingToFinish)
    .then(displayDoneMessage);

This addresses all of the problems I mentioned before:

  1. You do not have to read the definitions of those functions to understand the process
  2. If you want to insert a new step, you just add another `then` line.
  3. When you suddenly realize that processingIsDone() should actually be asynchronous, you don’t have to punch anything.
  4. More that we won’t cover here. Summary: simpler error handling, guarantees, easy expression of multiple paths, etc

The point of promises is to give us back functional composition and error bubbling in the async world.

Posted by Domenic Denicola Oct 14th, 2012

How Does It Work?

The basic functionality comes from abstracting that repeated pattern you could already see in the first code sample.

function waitTilAThingIsDone() {
   if (thingIsDone()) {
       doTheNextThing();
   } else {
       waitALittle();
   }
}

This is a common pattern, and the Promise essentially abstracts it, leaving you to fill in only the actual functionality. Promises can cover many different types of “do this [eventually]” patterns. Consider similar code with callbacks:

startFileUpload();
listenForFileUploadCompleteEvent(function() {
    displayProcessingMessage();
    listenForProcessingCompleteEvent(function() {
        displayDoneMessage();
    });
});

This callback code has most of the same problems the original setTimeout code did. It’s tightly coupled and inflexible, uncomposable, and hard to read.

Imagine trying to change it by inserting a UI confirmation and displaying an error if anything goes wrong:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
startFileUpload();
listenForFileUploadCompleteEvent(function(status) {
    if (status === 'success') {
        askUserIfTheyWantToContinue(function(response) {
            if (response === 'yes, continue') {
                displayProcessingMessage();
                listenForProcessingCompleteEvent(function(status) {
                    if (status === 'success') {
                        displayDoneMessage();
                    } else {
                        displayErrorMessage();
                    }
                }
            } else {
                displayDoneMessage();
            }
        });
    } else {
        displayErrorMessage();
    }
});

Ugh!

Promises work by abstracting away that branching, waiting logic. In the early examples it seemed like a trivial abstraction – it’s even a little hard to see exactly what logic is being removed from the individual functions themselves. On a larger scale, the benefits become very obvious.

1
2
3
4
5
6
7
8
9
10
11
startFileUpload()
.then(askUserIfTheyWantToContinue) //promise version
.then(function(responseFromUser){ //receives the result of the previous promise as an argument
    if (responseFromUser === 'yes, continue') {
        return displayProcessingMessage().then(waitForProcessingToFinish);
    } else {
        return true; //just forward immediately to the next effect
    }
}) 
.then(displayDoneMessage)
.fail(displayErrorMessage); //this will be called if ANY part of the chain fails

You can imagine much more complicated paths with more complicated branches being expressed this same way. It’s easy to re-use segments of the path that might appear in different places, as opposed to the original callback-based snippet which had to displayDoneMessage() manually at the end of every branch!

Ok, HOW DOES IT WORK? (with jQuery)

To encapsulate your code as a promise with the then interface, you can use jQuery’s Deferred object or any promise library. Q is a famous one that inspired Angular’s $q, etc.

Original, unpromising code:

function checkIfFileIsUploaded() { 
    listenForFileUploadCompleteEvent(function() {
        displayProcessingMessage();
        checkIfProcessingIsDone();
    });
}

If this was your old code, you might have smelled the multi-concern stink of an extra dependency: you not only had to figure out if the file was uploaded, you ALSO had to know what the next step was.

Promises with jQuery’s Deferred object:

function checkIfFileIsUploaded() {
    var deferred = $.Deferred();  //helps you construct a promise
 
    listenForFileUploadCompleteEvent(function() {
        deferred.resolve();  //this "resolves" the promise you've made, and triggers the `then` actions used outside
    });
 
    return deferred.promise(); //this object has the `then` property that can be called from outside this function.  
    // We don't return the whole deferred object because we don't want the user of this function to be able to call resolve() on it themselves
}

Basically, the unpromising code handled state itself. It kicked off an operation and said, “when you’re done, do these other things.” The outer function returned null immediately, so the caller of checkIfFileIsUploaded didn’t get anything to indicate success or failure.

The promising code was different. It also kicked off an operation, but it said, “when you’re done, tell this deferred object,” and then it returned a promise. The caller of checkIfFileIsUploaded got the promise and could add to the success / error behaviors itself.

Conclusion

In the last section I showed two different ways to implement checkIfFileIsUploaded. The complexity of that function was just about the same with and without promises, but with promises it needed less knowledge about the next step. By returning the promise it gave the CALLER control over the next step.

So, without much extra complexity, we can implement our functions with promises! Then we can get at the amazing simplicity of `then` and the other jQuery.Deferred properties, and the code that uses our implementations will be much simpler and much more flexible.

Leave a Reply

Your email address will not be published. Required fields are marked *