ES6 Tutorial: Escape Callback Hell with Promises in JavaScript11 min read
Asynchronous programming allows the computer to move to other tasks while waiting for asynchronous operations to complete. By doing so, we don’t have to put everything to 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 for 1 second to display the result in the first function, JavaScript first call the second function, later on, when the time has elapsed, it comes back and logs the result in the first
function, results in 'second'
, 'first'
.
But in some cases, if 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, making the code arduous to read; this process even becomes extremely painful if we add 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 is by using a promise. A promise doesn’t get rid of the callbacks, but it made the chaining of functions straightforward and simplifies 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 and 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 two parameters resolve and reject which are both functions. All of your asynchronous code will go inside this executor function. In case the operation has been 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 accepts 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 an 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 thenew
keyword to involve the promise’s constructor, and we pass to it a callback functionexecutor
. - The
executor
function we pass to this promise has two parametersresolve
andreject
, both of them are also callback functions. - If
isSuccessful
is evaluated to true, then we invokeresolve
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 finishes after a delay, so we don’t need to call the reject function. After the resolve function is invoked, we change the promise 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 take 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 parametercompleted
, inside this function, we return a promise; whether this promise is resolved or rejected depends on the value ofcompleted
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 thethen()
method on it, here we pass to it 2 callback functionssuccess
andfailingReason
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 is also possible to 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, 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 the 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 two 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.