The road from Callback hell to heavenly Promises

0

I am sure that every web developer is more or less aware of the javascript promises. Even for not pure front-end developers, like me, JS promises is a subject that can not easily left out when the discussion comes to asynchronous programming. I am not going to make an introduction to promises here, neither I am going to argue whether they consist a prettier syntax for callbacks or being something totally different. The purpose of this post is to give a few simple examples of how our coding passed from the simple use of callbacks to the advanced promises technique.

For the sake of simplicity and code readability, my examples will be based on jQuery. The scenario is simple: trying to retrieve some html pages through AJAX requests.

The synchronous version:   Waiting for I/O, and waiting….

<script type='text/javascript'>

    console.log("Let's execute the HTTP calls.");

    loadPage('http://localhost/demo1.html');
    loadPage('http://localhost/demo2.html');

    function loadPage(pageUrl){
        console.log('Loading '+pageUrl+' ...');

        $.ajax({
            url: pageUrl,
            method: "GET",
            async: false,
            success: function(data)
            {
                console.log('Request to '+pageUrl +' completed!');
            }
        });
    }

    console.log('Executing the code after HTTP calls');

</script>

Using callbacks the wrong way:   The best way to kill your CPU and you UI

<script type='text/javascript'>
    console.log("Let's execute the HTTP calls.");

    var asyncJobs = 2;
    var asyncJobsRemaining = asyncJobs;
    var asyncData = [];

    loadPage('http://localhost/demo1.html');
    loadPage('http://localhost/demo2.html');

    function loadPage(pageUrl) {
        console.log('Loading ' + pageUrl + ' ...');

        $.ajax({
            url: pageUrl,
            type: "GET",
            dataType: "jsonp",
            complete: function (data) {
                console.log('Request to ' + pageUrl + ' completed!');
                asyncData.push(data);
                asyncJobsRemaining--;
            }
        });
    }

    do {
        // do stuff while waiting
    }
    while (asyncJobsRemaining > 0);

    console.log('Executing the code after HTTP calls');
</script>

Naive callback:  How to get some headaches

<script type='text/javascript'>

    console.log("Let's execute the HTTP calls.");

    var asyncJobs = 2;
    var asyncJobsRemaining = asyncJobs;
    var asyncData = [];

    loadPage('http://localhost/demo1.html',codeAfterHttpCalls);
    loadPage('http://localhost/demo2.html',codeAfterHttpCalls);

    function loadPage(pageUrl,callback){
        console.log('Loading '+pageUrl+' ...');

        $.ajax({
            url : pageUrl,
            type: "GET",
            complete:function(data)
            {
                console.log('Request to '+pageUrl +' completed!');
                asyncData.push(data);
                asyncJobsRemaining--;
                if(asyncJobsRemaining == 0){
                    callback();
                }
            }
        });
    }

    function codeAfterHttpCalls(){
        console.log('Executing the code after HTTP calls');
    }

</script>

Theoritically, we have made some progress. Executing the requests in parallel, without blocking the JS thread. But how scalable it is?

Multiple callbacks:   Approaching the callback hell

<script type='text/javascript'>

    console.log("Let's execute the HTTP calls.");

    var asyncJobsRemaining = 2;
    var asyncData = [];

    loadPage('http://localhost/demo1.html',codeAfter2HttpCalls);
    loadPage('http://localhost/demo2.html',codeAfter2HttpCalls);

    function codeAfter2HttpCalls(){
        console.log('Executing the code after the first 2 HTTP calls');

        // Process the data from the first 2 HTTP calls

        asyncJobsRemaining = 2;

        loadPage('http://localhost/demo1.html',codeAfter4HttpCalls);
        loadPage('http://localhost/demo2.html',codeAfter4HttpCalls);

    }

    function codeAfter4HttpCalls(){
        console.log('Executing the code after the first 4 HTTP calls');

        // Process the data from the first 4 HTTP calls
    }

    function loadPage(pageUrl,callback){
        console.log('Loading '+pageUrl+' ...');

        $.ajax({
            url : pageUrl,
            type: "GET",
            complete:function(data)
            {
                console.log('Request to '+pageUrl +' completed!');
                asyncData.push(data);
                asyncJobsRemaining--;
                if(asyncJobsRemaining == 0){
                    callback();
                }
            }
        });
    }

</script>

You can guess the problem, right? You cannot go very deep without killing your readability, your maintainability and…yourself!

Reaching the “promised” land:  

<script type='text/javascript'>

    console.log("Let's execute the HTTP calls.");

    var promises = [];
    promises.push(asyncPageLoad('http://localhost/demo1.html'));
    promises.push(asyncPageLoad('http://localhost/demo2.html'));

    // Promise.all() takes an array of promises and fires one callback
    // once they are all resolved
    Promise.all(promises).then(function SuccessHandler(results) {
        console.log('Executing the code after HTTP calls');
        console.log(results);
    }, function ErrorHandler(error) {
        console.log('Executing the code after HTTP calls');
        console.log(error);
    });

    // We promisified the loadPage()
    // We don't care about error handling, wo we are not using the
    // reject callback
    function asyncPageLoad(pageUrl) {
        return new Promise(function(resolve, reject) {
            console.log('Loading '+pageUrl+' ...');

            $.ajax({
                url : pageUrl,
                type: "GET",
                complete:function(data) {
                    console.log('Request to '+pageUrl +' completed!');
                    return resolve(data);
                }
            });
        });
    }

</script>