OdeToCode IC Logo

Tips For JavaScript Promises

Wednesday, March 26, 2014

Q tipsThere are couple scenarios where I occasionally see too much JavaScript code being written with promises.

For the examples, let’s assume we are working with a simple function returning a promise like the following doWork function. This code is using q, but everything here is also true for Angular’s $q service.

var doWork = function(){
    var deferred = Q.defer();

    setTimeout(function(){
        deferred.resolve("done");
    }, 1000);

    return deferred.promise;
};

Invoking doWork to get a result is simple.

var onSuccess = function(result) {
    console.log("Success: " + result);
};

var onError = function(reason) {
    console.log("Error: " + reason);
}

doWork().then(onSuccess, onError);

Wrapping A Promise Function

The first scenario that often involves too much code is the scenario where you want to wrap a promise to add some additional calculations after the initial promise resolves, but before returning a promise to a higher level component. Examples would include processing an HTTP response to add caching or some data manipulation.

With the doWork function, we might just want to add some additional text (“with added value”) to the return.

/* don't do this */
var workWrapper = function() {
    var defer = Q.defer();
    
    doWork().then(function(result) {
        defer.resolve(result + " with added value");
    }, function(reason){
        defer.reject(reason);
    });

    return defer.promise;
};

The above code goes to a lot of extra work to create a new deferred object and handle errors, but the same result could be achieved with less code.

/* do this instead */
var workWrapper = function() {
    return doWork().then(function(result) {
        return result + " with added value";
    });
};

An error will still prorogate correctly, and the then function will capture then plain return value (a string) and wrap it in a promise. To the caller, it’s still easy to grab the final result.

workWrapper().then(onSuccess, onError);

Do I Have A Promise Or Not?

A similar scenario exists when you have a function that may or may not return a promise. For example, if request is made for some data but the data is found in a cache, a function can return the data immediately. If the data isn’t in a cache the function might need to make an asynch call to fetch the data and return a promise for the future.

The below function simulates this scenario by returning a raw string value approximately half the time, and a promise the rest of the time.

var promiseOrValue = function() {
    if(Math.random() < 0.5) {
        return "done early";
    }
    else {
        return doWork();
    }
};

The easiest way to handle this scenario is not to test the return value to see if the value is a promise, but treat everything as a promise, which is easy to do with Q.when.

Q.when(promiseOrValue()).then(onSuccess, onError);

If you own the function, it’s even nicer if the function always returns a promise, even for data that is immediately available.

var alwaysAPromise= function() {
    if(Math.random() < 0.8) {
        return Q.when("done early");
    }
    else {
        return doWork();
    }
};

Then invoking the function is as easy as invoking the original doWork function.

alwaysAPromise().then(onSuccess, onError);

A Plunkr

If you want to experiement with promises, I put together a Plunkr using Jasmine to describe some of these behaviors with tests.