Introduction

Asynchronous processing is one of the most fundamental and elementary concepts of JavaScript, yet one which troubles beginner developers both in understanding it and actually implementing it.

The architecture of JavaScript utilises essentially two things to work around asynchronous operations and track when they complete or fail - they are events and callbacks.

An event is simply an action occuring on a webpage while a callback is a function passed to another function, as an argument.

Both these ideas are what enable handling asynchronous tasks in the language - for example we attach events to XMLHttpRequest objects to hear for state changes, abortion, or progress of the request; or pass callbacks to things like reading a file in Node.js, to be fired when the reading operation completes.

While they offer superb convenience to developers in writing basic asynchronous code, events and callbacks start to crumble in their syntactic structure as soon as the underlying code becomes more involved. With more code, comes less maintainability and more snag.

Let's explore all this in detail.

Pitfalls of callbacks

To put up a practical-level discussion on the complications of using callbacks, we would need a real example that uses them, and is itself used a lot. Perhaps the best choice would be AJAX.

AJAX stands for Asynchronous JavaScript and XML, and is a technique of sending out HTTP requests from the browser asynchronously, without requiring a full page reload. It's used extensively by many large web applications - such as Facebook, Google etc. - to add a whole new level of interactivity and modernism to their user interfaces.

For a detailed guide to AJAX please refer to our AJAX Course.

Let's consider a very basic AJAX call. Suppose you want to request the names.txt file shown below using AJAX:

names.txt
Only one file linked for now.
foods.txt

The code to do this, will be something like the following:

var xhr = new XMLHttpRequest();
xhr.open("GET", "names.txt", true);
xhr.onload = function() {
    if (this.status === 200) {
        document.write(this.responseText);
    }
}
xhr.send();

Now although this works well, as soon as the need be to perform nested AJAX requests (one in another) we'll end up with a strenous-to-maintain block of code. An example would be worth the discussion.

Imagine that after successfully requesting the names.txt file above, we want to separately request the file mentioned within it i.e foods.txt.

foods.txt
Pizza
Pasta
Cookies

What we'll need to do in order to accomplish this, is to process the response of names.txt, extract the filename from it, and then finally request for it with a new AJAX call.

Something like:

var xhr = new XMLHttpRequest();
xhr.open("GET", "names.txt", true);
xhr.onload = function() {
    if (this.status === 200) {
        // process the response
        var filename = this.responseText.split("\n")[1]; // extract the filename

        // new AJAX request
        var xhr2 = new XMLHttpRequest();
        xhr2.open("GET", filename, true);

        xhr2.onload = function() {
            if (this.status === 200) {
                alert(this.responseText);
            }
        }
        xhr2.send();
    }
}
xhr.send();
Here we're assuming that names.txt contains a filename in its second line.

Inside the onload handler for the first xhr request to names.txt (in line 3), we have the logic laid out for the second xhr request to foods.txt, which when completes, calls the onload handler in line 12 - where we finally output the content of the foods.txt file.

Notice how the indentations are starting to become longer and longer, pushing out sub-level code further to the right. One might argue and say that, we can create a separate function and put all the code for the second AJAX call within it, and thus avoid the indentation - as shown below:

function secondRequest(xhr) {
    var filename = xhr.responseText.split("\n")[1];

    var xhr2 = new XMLHttpRequest();
    xhr2.open("GET", filename, true);

    xhr2.onload = function() {
        if (this.status === 200) {
            alert(this.responseText);
        }
    }
    xhr2.send();
}

var xhr = new XMLHttpRequest();
xhr.open("GET", "names.txt", true);
xhr.onload = function() {
    if (this.status === 200) {
        // process the response
        secondRequest(this)
    }
}
xhr.send();

Everything is the same here as before, except for that this time the logic for the second xhr request is defined in a function, secondRequest(); and not directly.

Even better, someone can come up with the following code - defining just a single function to create a new AJAX request with the given filename and callback (to fire when the request completes successfully):

function ajaxRequest(filename, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", filename, true);
    xhr.callback = callback;
    xhr.onload = function() {
        if (this.status === 200) this.callback();
    }
    xhr.send();
}

ajaxRequest("names.txt", function(e) {
    var filename = this.responseText.split("\n")[1];
    ajaxRequest(filename, function() {
        alert(this.responseText);
    });
});
The callback property is given to the xhr object (in line 4) in order to invoke it with this equal to the xhr object.

Good thinking! But this can only suffice until error-handling and further nested AJAX calls aren't required in the code. As soon as either one enters the game, it's simply game over!

Consider the snippet below extending our previous AJAX code with an error-handling logic:

function ajaxRequest(filename, callback, errorCallback) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", filename, true);
    xhr.callback = callback;
    xhr.onload = function() {
        if (this.status === 200) this.callback();
    }
    xhr.onerror = errorCallback;
    xhr.send();
}

ajaxRequest("names.txt", function(e) {
    var filename = this.responseText.split("\n")[1];

    ajaxRequest(filename, function() {
        alert(this.responseText);
    }, function(e) { console.error("Error!") });

}, function(e) { console.error("Error!") });

Notice how the onerror handler is redefined for no reason whatsoever - the error handling logic is the same i.e to throw an error saying "Error!", however the functions to do this are different!

A simple workaround would be to create a separate error handler function like handleError() below, and pass it to the individual ajaxRequest() functions:

function handleError() {
    console.error("Error!");
}

ajaxRequest("names.txt", function(e) {
    var filename = this.responseText.split("\n")[1];
    ajaxRequest(filename, function() {
        alert(this.responseText);
    }, handleError);
}, handleError);

But even then the overall syntax of the code would remain cluttered. Just look at the code above - don't you think it looks way overwhelming?

Nonetheless, this is just one of the many problems with the callback syntax - the story doesn't end here.

For a split second imagine that foods.txt also requires calling another file such as pizza.txt. Then the code will approach what is commonly known as a callback hell:

pizza.txt
Chicken Tikka Pizza
BBQ Pizza
Cheese Pizza
ajaxRequest("names.txt", function(e) {
    var filename = this.responseText.split("\n")[1];
    ajaxRequest(filename, function() {
        var filename = this.responseText.split("\n")[1];
        ajaxRequest(filename, function() {
            alert(this.responseText);
        }, handleError);
    }, handleError);
}, handleError);

See how the callbacks are starting to become tangled and pushed continuously rightwards. With a couple of these we would simply end up with a callback hell, also known as the pyramid of doom:

func1(function(data) {
    func2(function(data) {
        func3(function(data) {
            func4(function(data) {
                // some code
            });
        });
    });
});

This looks ugly and is, no doubt, difficult to maintain.

Bundle error-handling code into it, and you'll 'put yourself into the real hot waters'.

However these aren't the only cons in using callbacks - there exist more; ones that definitely need another way to be accomplished. For example, consider the task of requesting multiple files in one go and then executing a function once all of them are fetched completely.

Take your time, and try to think on this problem in the callback style for a while; see where you end up!

Promises step in

Solving all the problems discussed in the section above, steps in the concept of promises. So what exactly is a promise?

Defining it with the idea behind it:

A promise is a means of simplifying the task of writing complex asynchronous code.

But this ain't the definition that exactly defines what is a promise. It's just a convenient way to look at it.

In technical terms:

A promise is an object that represents the success or failure of a given operation, usually an asynchronous operation.

OK, now this might be too much to digest at a time!

In layman terms, a promise is simply a means of placeholding the completion or failure of a given task. Usually, this task is an asynchronous task - what promises are really meant for.

Promises aren't meant for synchronous tasks.

'Placeholding' means that once the promise executes its given task, it serves to hold onto the result which can be a success or a failure.

Using this held-on outcome, the program can then carry out respective actions, such as further processing the data in the case of a success or logging an error in the case of a failure.

Don't worry if you couldn't understand a word in this discussion. Promises are verily one of the concepts in JavaScript that don't make any sense at all, unless and until you rigorously experiment with them.

If the definitions go above your head, which they normally should, just ignore them for now. Just stay focused on learning the internal behaviour behind promises and you'll soon be writing definitions of your own!

So where to learn these? Just keep reading...

Coming back to the topic, a promise in JavaScript is represented by the Promise() constructor. Any promises you need to make ought to utilise the Promise object, always.

Talking about how to actually create a promise and then work with it, we will discuss the bits and pieces to this in detail in the next chapter.

For the moment let's get a quick overview of what promises offer us i.e the benefits of using promises.

Benefits of using promises

First of all promises mitigate the extra levels of indentation, we saw earlier, by a mechanism for attaching callbacks instead of passing them to another function.

Secondly, error-handling in promises, is a lot more concise and maintainable than error-handling in callbacks. Promises are built upon the conventional try..catch model used to respond to thrown exceptions, and thus offers more convenience to developers in writing exception-handling code.

Moreover, the promise syntax relates to English language very closely, consequently making linked asynchronous calls seem way more meaningful and comprehensive to understand.

The heart of this feature is the then() method, as we shall see in the next chapter.

The story doesn't end here - it's just that we all will get overwhelmed seeing the minute pros promises offer us, all together in one place!

It's best to keep things simple for now, leaving the rest of the journey to be discovered by you. So are you ready to begin?