13

I'm reading this article and the section on the promise abstraction seems a little overly complicated to me. The following is given as an example:

requestSomeData("http://example.com/foo") // returns a promise for the response
    .then(function(response){ // ‘then’ is used to provide a promise handler
        return JSON.parse(response.body); // parse the body
    }) // returns a promise for the parsed body
    .then(function(data){
        return data.price; // get the price
    }) // returns a promise for the price
    .then(function(price){ // print out the price when it is fulfilled
        print("The price is " + price);
    });

It seems to me that the following could provide the same result with fewer lines of code:

requestSomeData("http://example.com/foo")
    .requestHandler(function(response){
        // parse the body
        var data  = JSON.parse(response.body);

        // get the price
        var price = data.price;

        // print out the price
        print("The price is " + price);
    });
Darrell Brogdon
  • 6,843
  • 9
  • 47
  • 62
  • 2
    You are right, there is no point in using promises for synchronous operations. So the result should be equal. But then it is an example and illustrates to usage of the promise. For the code that is running after your example there is indeed a difference. If you need to run something after the example than you can do it (by using the promise approach) without knowing anything about what the example code is doing – Norbert Hartl Mar 21 '11 at 08:05

4 Answers4

17

While it is true that both will ultimately accomplish the same thing, the difference is that your second example is not asynchronous. For example, consider what happens if JSON.parse(...) turns out to be an extremely expensive operation; you'll have to hang until everything's finished, which may not always be what you want.

That's what promises get you: the powerful ability to defer the computation of the right answer until a more convenient time. As the name suggests, the construct "promises" to give you the result at some point, just not necessarily right now. You can read more about futures and promises work on a larger scale here.

John Feminella
  • 303,634
  • 46
  • 339
  • 357
  • 2
    What is a convenient time here? If the operation is extremely expensive and JSON.parse is a piece of javascript code it will hang anyway. The difference is only that with the promise you can finish the actually running functions. – Norbert Hartl Mar 21 '11 at 07:46
  • 3
    It is true that the parsing will take the same amount of time regardless of whether it is computed synchronously or asynchronously. However, if you take the time to implement your parser in such a way that it predictably yields to the event loop after completing small pieces of the operation, other asynchronous code can run asynchronously between each chunk. This makes an application more responsive, not faster. – Kris Kowal Sep 23 '11 at 18:32
  • Or JSON.parse could be a native method and execute on another thread – Jan Oct 23 '11 at 16:16
  • @Jan That does not help. JSON.parse is a native method already. – Camilo Martin Nov 18 '12 at 00:21
  • This is more for future people reading your comment. You could create a web worker that calls the native JSON.parse to prevent the UI thread from hanging. This would involve loading the new worker, attach listener for new messages, calling JSON.parse in worker, posting the result from worker, and then pass the result to the original code that needed it. Wrapping this code in a promise abstracts the process and makes it easier to code/refactor/read what the code does without debugging the mess. – jcbelanger Jun 14 '13 at 20:57
3

Let's compare the promise example to a pure Javascript example:

// First we need a convenience function for W3C's fiddly XMLHttpRequest.
// It works a little differently from the promise framework.  Instead of 
// returning a promise to which we can attach a handler later with .then(),
// the function accepts the handler function as an argument named 'callback'.

function requestSomeDataAndCall(url, callback) {
    var req = new XMLHttpRequest();
    req.onreadystatechange = resHandler;
    req.open("GET", url, false);
    req.send();
    function resHandler() {
        if (this.readyState==4 && this.status==200) {
            callback(this);
        } else {
            // todo: Handle error.
        }
    }
}

requestSomeDataAndCall("http://example.com/foo", function(res){
    setTimeout(function(){
        var data = JSON.parse(res.responseText);
        setTimeout(function(){
            var price = data.price;
            setTimeout(function(){
                print("The price is "+price);
            },10);
        },10);
    },10);
});

As Norbert Hartl pointed out, JSON.parse() will hang the browser for large strings. So I used setTimeout() to delay its execution (after a pause of 10 milliseconds). This is one example of Kris Kowal's solution. It allows the current Javascript thread to complete, freeing up the browser to present DOM changes and scroll the page for the user, before the callback runs.

I hope the commonjs promise framework also uses something like setTimeout, otherwise the later promises in the article's example will indeed run synchronously as feared.

My alternative above looks pretty ugly, with the later processes requiring further indentation. I restructured the code, so that we can provide our process chain all in one level:

function makeResolver(chain) {
    function climbChain(input) {
        var fn = chain.shift();      // This particular implementation
        setTimeout(function(){       // alters the chain array.
            var output = fn(input);
            if (chain.length>0) {
                climbChain(output);
            }
        },10);
    }
    return climbChain;
}

var processChain = [
    function(response){
        return JSON.parse(response.body);
    },
    function(data){
        return data.price; // get the price
    },
    function(price){
      print("The price is " + price);
    }
];

var climber = makeResolver(promiseChain);
requestSomeDataAndCall("http://example.com/foo", climber);

I was hoping to demonstrate that traditional forward-passing of callbacks in Javascript is pretty much equivalent to promises. However after two attempts I appear to have shown, with reference to the neatness of the code in the original example, that promises are a far more elegant solution!

1

The second snippet is vulnerable to denial of service attack because example.com/foo can just return invalid json to crash the server. Even empty response is invalid JSON (though valid JS). It's like mysql_* examples with glaring SQL injection holes.

And the promise code can be improved much as well. These are equal:

requestSomeData("http://example.com/foo") // returns a promise for the response
    .then(function(response){ // ‘then’ is used to provide a promise handler
        // parse the body
        var data  = JSON.parse(response.body);

        // get the price
        var price = data.price;

        // print out the price
        print("The price is " + price);
    });

And:

requestSomeData("http://example.com/foo")
    .requestHandler(function(response){
        try {
            var data = JSON.parse(response.body);
        }
        catch(e) {
            return;
        }

        // get the price
        var price = data.price;

        // print out the price
        print("The price is " + price);
    });

If we wanted to handle the error, then these would be equal:

requestSomeData("http://example.com/foo") // returns a promise for the response
    .then(function(response){ // ‘then’ is used to provide a promise handler
        // parse the body
        var data  = JSON.parse(response.body);

        // get the price
        var price = data.price;

        // print out the price
        print("The price is " + price);
    }).catch(SyntaxError, function(e) {
        console.error(e);
    });

and:

requestSomeData("http://example.com/foo")
    .requestHandler(function(response){
        try {
            var data = JSON.parse(response.body);
        }
        catch(e) {
            //If the above had a typo like `respons.body`
            //then without this check the ReferenceError would be swallowed
            //so this check is kept to have as close equality as possible with
            //the promise code
            if(e instanceof SyntaxError) {
                console.error(e);
                return;
            }
            else {
                throw e;
            }
        }

        // get the price
        var price = data.price;

        // print out the price
        print("The price is " + price);
    });
Esailija
  • 138,174
  • 23
  • 272
  • 326
0

One might also add that the advantage of the first version over the second is that it separates different operations in the refinement chain (the functions don't have to be written in-place either). The second version mixes both the low-level parsing with application logic. Specifically, using the SOLID principles as guidelines, the second version violates both OCP and SRP.