ES6 Tutorial: Escape Callback Hell with Promises in JavaScript11 min read

Asynchronous programming is a pretty challenging topic in JavaScript. Basically, it allows the computer to move to other tasks while waiting for asynchronous operations to complete. By doing so, we don’t have put everything in a halt because of asynchronous action.

As we saw in the previous article, callbacks are typically useful in asynchronous programming:

function first() {
    setTimeout(() => {
        console.log('First')
    }, 1000);
}

function second() {
    console.log('second');
}
first();
second();

Instead of waiting 1 second to display the result in the first function, JavaScript firstly call the second function, later on when the time is elapsed, it comes back and logs the result in the first function, results in 'second', 'first'.

But in some cases, you have too many nested callbacks, your code might end up and look like this:

function callbackHell() {
    setTimeout(function() {
        console.log('First')
        setTimeout(function() {
            console.log('Second')
            setTimeout(function() {
                console.log('Third');
                setTimeout(function() {
                    console.log('Fourth');
                    setTimeout(function() {
                        console.log('Fifth');
                    }, 2000)
                }, 2000);
            }, 2000);
        }, 2000);
    }, 2000);
};
callbackHell();

This code will basically display the message to the console every 2 seconds from top to bottom. However, there are too many callback functions nested inside others make the code is arduous to read, this process even becomes extremely painful if we added some extra code inside the body of each callback function.

This example above is an example of “callback hell” and there is a better way to perform asynchronous action, which by using a promise. A promise doesn’t get rid of the callbacks, but it made the chaining of functions straightforward and simplified the code, making it much easier to read.

Promises in JavaScript

In JavaScript, Promises are objects represent the eventual outcome of an asynchronous action. At a time, a promise can be one of 3 states:

  • Pending: This is an initial state, the operation has not yet completed, neither fulfill or rejected.
  • Fulfill: the operation has been completed successfully, the promise now has the resolved value.
  • Rejected: the operation has been failed due to some kind of errors.

We have a real-world analogy for visualizing JavaScript promises, if a washing machine has the states as a promise, then:

  • Pending: the washing machine is running but has not yet finished its operations.
  • Fulfill: it has finished its operation successfully, gives us fragrant clothes.
  • Rejected: oops, it received no soap, gives us back dirty clothes.

When a promise is either fulfill or rejected, this promise is considered settled and then we can perform some further actions by calling the .then() method (which we will discuss below). Moving back to the washing machine, if it is fulfilled then we can take the clothes out and take it to dry. But if it is rejected, we can take alternative actions such as providing soap to the machine.

Constructing a Promise

We have learned some theory about Promises, now, let’s create our first promise. A promise can be created by involving its constructor with the new keyword:

var myPromiseObject = new Promise(executor);

The executor is a callback function we pass in and it runs automatically when the promise’s constructor is called and it is a custom code that ties to an outcome of a promise. This executor function takes 2 parameters resolve and reject which are both functions. All of your asynchronous code will go inside this executor function. In case if the operation has completed successfully, the resolve function will be invoked and it changes the state of this promise from pending to fulfilled. If there is an error occurs during this operation, there will be an invocation to reject function and it will change the promise’s status from pending to rejected.

Both resolve() and reject() function accept one argument, if the resolve function is called,  the promise‚Äôs resolved value will be set to the argument passed into resolve(). Otherwise if reject() is called, then the value we pass in this function will dictate the rejecting reason for this promise.

Note that resolve and reject are callback functions defined by JavaScript, when the Promise constructor runs, JavaScript will pass it own resolve and reject functions to the executor function.

Let’s look at an example of executor function:

var executor = (resolve, reject) => {
    if (isSuccessful) {
        resolve('This promise completed successfully!')
    } else {
        reject('Errors occurred')
    }
}
var myPromiseObject = new Promise(executor);

Let’s break down what’s happening in this code fragment:

  • First, we declare a variable myPromiseObject and assign it to a promise, we create a new promise by using the new keyword to involve promise’s constructor and we pass to it a callback function executor.
  • The executor function we pass to this promise has 2 parameters resolve and reject, both of them are also callback functions.
  • If isSuccessful is evaluated to true, then we invoke resolve function with a string value 'This promise completed successfully'
  • If not, we call the reject function and give it a string 'Errors occurred'.

Consuming a Promise

Knowing how to create a promise is great so far, but most of the time, knowing how to consume a promise is pivotal. But before we learn how to consume a promise, let’s create a simple asynchronous action and put it inside a promise:

var myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Some data');
    }, 1000);
});

Here again, we use the setTimeout function which uses callback functions to schedule tasks to be performed after a delay, in this case, 1 second. In this case, we don’t create a separate executor function, but instead, we create the executor function right inside this promise. Since the setTimeout function is impossible to fail, the timer always finished after a delay so we don’t need to call the reject function. After the resolve function is invoked, we change the promise which stored in myPromise from pending to fulfilled (resolved):

When a promise is no longer pending, its status has been shifted to either resolved or rejected. At that time, the promise is considered settled and we can do further actions by using then(), catch(), finally() methods on this promise.

The .then() method

The initial state of a promise is pending, but it is guaranteed to be settled in the future (either resolved or reject). What should we do next? We can use the .then() method to perform some actions afterward. In our case of the washing machine promise, we can aptly apply the .then() method as follow:

  • If there is no soap, then we provide the soap and run the washing machine again.
  • If the soap is provided and the washing machine has worked properly, then we will take clothes to dry.

The .then() is a method taking 2 callback functions as arguments onFulfilled (success handler) and onRejected(failed handler). The onFulfilled function is invoked when the promise is fulfilled. If the promise is rejected, the onRejected function will be called. And this method always returns a promise.

promiseObject.then(onFulfilled, onRejected);

There is no obligation that we must pass all two methods onFulfilled or onRejected to this method, we can pass them both, either, or we can omit both functions. This allows for flexibility, but it can be trickier to debug. If in case there is no appropriate handler, the .then() method just returns the value as the settled value in the promise it was called.

function sweepTheFloorPromise(completed) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => {
            if (completed) {
                resolve("I have done with sweeping, now the floor is clean!")
            } else {
                reject(Error("There is no broom!"));
            }
        }, 3000);
    });
}
var sweepTheFloor = sweepTheFloorPromise(true);

sweepTheFloor.then(
    success, failingReason 
);

function success(message) {
    console.log(message);
}

function failingReason(reason) {
    console.log(reason);
}

Let’s break down what’s happening:

  • We create a function called sweepTheFloorPromise which takes one parameter completed, inside this function, we return a promise, whether this promise is resolved or rejected depends on the value of completed argument.
  • Underneath we create a variable and assign it to the result of calling the sweepTheFloorPromise function.
  • Because sweepTheFloor is a promise, we can call the then() method on it, here we pass to it 2 callback functions success and failingReason corresponding to the success handler and the failed handler.

If we run this code on our browser, here is what we get:

Since the promise inside the sweepTheFloorPromise is resolved, hence the success handler function sucess will be invoked with this promise’s resolved value (again, the promise inside the sweepTheFloorPromise function).

It also possible to either schedule a callback to handle the fulfilled or rejected case only, if we just want to run the fulfilled case:

sweepTheFloor.then(success);

Or we only want the rejected case:

sweepTheFloor.then(
    undefined, failingReason
);
// or 
sweepTheFloor.then(
    null, failingReason
);

Instead of passing both handlers into one then, we can chain a second .then() with a failure handler to a first .then() with a success handler and both cases will be handled.

sweepTheFloor
    .then(success)
    .then(null, failingReason)
 

Since JavaScript doesn’t mind the whitespace, we can put each then method in this chain on a new line to make it easier to read.

We can create a more readable code for handling the onRejected value by using the .catch() method.

The .catch() method

The catch() method accepts only one argument which is onRejected value. In the case of a rejected promise,  this failure handler will be invoked with the reason for rejection. Using catch() accomplishes the same thing as then() with only failed handlers:

sweepTheFloor
    .then(success)
    .catch(failingReason)

The .finally() method

Sometimes you don’t care whether a promise is fulfilled or rejected and want to execute a same piece of code, we can use the .finally() method, this method also takes a callback function as a parameter and this method also returns a promise:

sweepTheFloor
    .then(success)
    .catch(failingReason)
    .finally(doSomething)

Whether the is a rejected promise or successfully one, after the promise has settled, the finally method always is called, doSomething is a callback function we pass in this method.

Escape Callback hell with Promises

We have learned quite a lot about promises in JavaScript, now we can apply what we have acquired to transform a callback hell to a piece of code with the same purpose but much easier to read.

For example, we have a simple code for simulating the OAuth process with multiple nested callbacks:

function oauth() {
    login(username, password, (accept) => {
        if(accept && isValid(username, password)){
            getCode((clientID, responseType, redirectURI) => {
                if(isValid(clientID, responseType, redirectURI)){
                    getAccessToken((clientID, clientSecret, grantType, code, redirectURI) => {
                        if(isValid(clientID, clientSecret, grantType, code, redirectURI)){
                            getRequestData(accessToken);
                        }else{
                            alert('Cannot get the access token');
                        }
                    })
                }else{
                    alert('Cannot get the auth code');
                } 
            })
        }else{
            alert('Please accept the auth process!')
        }
    })
}

This code is really hard to read, but we can feel relieved by using promises to achieve our intention:

var login = function(username, password) {
    return new Promise((resolve, reject) => {
        if (isValid(username, password)) {
            resolve('Logged in');
        } else {
            reject("Something is wrong, cannot sign in!");
        }
    });
}
// functions inside the `then` methods below are all callback functions.
var oauth = login('abc', 123)
    .then(getTheCode)
    .then(getAccessToken)
    .then(getData)
    .then(displayData)
    .catch(someError);

This example is not complete, thus it will not run. But this example demonstrates in some situations, if there are too many nested callbacks, then promises are good replacements. The process can be as follow, first, the user needs to log in to an account, if the user has logged in to an account, which means this promise has been fulfilled, we then call the getTheCode function, and after we get the code, we can exchange this with the access token…To the end, if there is a rejected promise, then the catch method will be called.

Summary

Promises are JavaScript objects that represent the eventual result of an asynchronous operation. A promise has 3 states: pending, resolved, and rejected, a promise is considered settled if it is either resolved or rejected. To create a new promise we use the new keyword to invoke its constructor, the executor function has 2 parameters, resolve and reject. The .then() method contains the logic if a promise is resolved. The .catch() method accepts one parameter and is called if there is a rejected promise. Instead of multiple nested callbacks, we can a chain of .then() method. The .finally() method will always be called when a promise is settled.

Previous Article
Next Article

Sign up for newsletter

* indicates required

Categories

Archives