Handling Asynchronous Code Using JavaScript Promises.

Lexical Magazine
The Startup
Published in
6 min readSep 30, 2020

Asynchronous operations before promises.

Sometimes you have a function or operation that takes a considerable amount of time to finish like a network/service request to fetch some data or resource but then you want to continue executing the rest of your program and then use the data returned from the operation whenever the process completes.

Instead of waiting on the call to complete you can let the rest of your program that doesn’t rely on the data from the operation to continue running and only run the dependent code after the operation returns a value.

Promises are a fairly new API in JavaScript so we will first look at how asynchronous operations were done before promises. The most common way was to pass in callback functions that were called and ran after the success or failure of the asynchronous function.

We will use a simple social media application page where a user’s basic information and list of their friends are displayed as an example. To get all the data needed to display the page we might need to call multiple services but we might not be able to call all of them simultaneously as some calls will likely depend on data from another service.

In the case of our imaginary social media app we might need to call the login service first and then call the user information service once the user’s credentials are validated. If the login is successful then we might want to use the user’s id to fetch their friends list to display on the page along with other data.

Here is some code we might use to get the data we need to render the page. Some code has been omitted for brevity.

function successCallback(userData) {
// display user's basic information and a list of their friends // if the operation completed successfully
}
function errorCallback(errorMessage) {
// display the error message.
}
function login(successCallback, errorCallback, userName, password) {
// do some operation here then run the callback function
// call the user service
callUserInfoService(userName, password, function(user, error) {
if(user) {
// get the user's friends
getUserFriends(user, function(friends, error) {
// call the friends service
if(friends) {
successCallback({
friends : friends
});
} else {
errorCallback(error);
}
});
} else {
errorCallback(error);
}
});
}
login(successCallback, errorCallback, userName, password);

We first define the success and failure callback functions which will be called if the user is successfully authenticated or if something goes wrong respectively. We then pass the callbacks to the login function along with the user name and password.

Since the services calls take time and are dependent on the result of the previous one we can’t just run one after the other. We have to keep nesting the service calls and passing more callback functions to each call to make sure they only run after the completion of the previous one.

As you can see this can get messy real quick if we have to make multiple dependent calls. This design pattern is infamous in the JavaScript world and is commonly referred to as callback hell as the nested callbacks will get deeper and harder to keep track as the number of operations increase.

We also have to have similar error handling repeated for each call which violates the principle of keeping our code DRY.

Luckily we have promises to rescue us from this callback hell but before we dig into any code that shows the promise implementation we should take a look at what promises are and how they work first.

What is a promise?

In essence a promise is a JavaScript object returned right away but its final value will only be known in the future upon the completion of an asynchronous operation. The eventual value will reflect the success or failure of the operation.

The promise is non blocking and code execution continues while waiting for the result of the asynchronous task.

The state of the promise.

A promise can be in one of the following three states:

  1. Pending — The operation is still in progress and still waiting for it to complete.
  2. Fulfilled — The operation was successfully completed.
  3. Rejected — Something went wrong with the operation and an error message is returned.

Promise instance methods.

An instance of a promise has methods that are attached to it that run after the promise is resolved/settled.

  • promise.then() runs when the operation is successfully completed the promise is fulfilled.
  • promise.catch() will run if there was an error running the operation and the promise is rejected.
  • promise.finally() will run when the promise is resolved/fulfilled regardless of success or failure. This is where you should run any code that is independent of the result but needs to be executed.

Refactoring to use promises instead of callbacks.

To create a promise we just call the promise constructor and pass in a function with the resolve and rejection functions. We call these when the operation is successful or fails. Calling these functions will resolve the promise in turn calling the .then or .catch methods on the promise. This will also update the state of the promise from pending to fulfilled or rejected depending on the outcome.

Below is a simple example of how to initialize and use a promise.

const promise1 = new Promise((resolve, reject) => {
// Do an operation here and either resolve or reject depending on the operation outcome;
if(success) {
resolve(data);
} else {
reject('rejection message');
}
});
promise1.then((data) => {
// use the returned data here
}).catch((error) => {
// there was an error so we deal with it here
});

Now that we know how to initialize and use a promise lets refactor the previous code for our imaginary app to use promises instead of callbacks.

function login(userName, password) {
return new Promise((resolve, reject) => {
// call the service and use the returned data
if(userId) {
resolve(userId)
} else {
reject('there was an error logging in the user');
}
});
}
function callUserInfoService(userId) {
return new Promise((resolve, reject) => {
// call the service and use the returned data
if(userData) {
resolve(userData);
} else {
reject('there was an error getting the user data');
}
});
}
function getUserFriends(userId) {
return new Promise((resolve, reject) => {
// call the service and use the returned data
if(friends) {
resolve(friends);
} else {
reject('there was an error getting the user data');
}
});
}
const loginPromise = login(userName, password);loginPromise
.then(callUserInfoService(userId))
.then(getUserFriends(userId))
.catch((error) => {
// handle errors here
});

I skipped some code for brevity so please feel free to let me know in the comments if I need to clarify anything.

We first promisified the service call functions to return a promise. This allows us to chain the service calls using the then function because each function returns a new promise which we then just call the then method on.

We also have a single catch method at the end of the call to catch any errors that might occur in any of the service calls when the promise is rejected.

As you can see being able to chain the operations is much cleaner and easier to read and follow than with the callbacks. We can clearly see which calls run after which and how they are dependent on each other.

In practice you rarely have to construct your own promises as most newer libraries and even native functions and APIs will already return a promise. You can simply just call the then method directly on the returned promise. A good example is the native fetch API which is used to make network requests.

Below is an example fetch request. The API returns a promise already so you can just run the promise methods directly.

fetch('https://example.com').then(() => {
// success
}).catch((error) => {
// failure
});

Promises are a great addition to any developer’s toolbox to simplify any asynchronous operations but since ECMAScript 2017 there is an even simpler and cleaner way to handle this using async/await. I’ll be doing a tutorial on async/await soon and I’ll post a link here once it’s ready.

Thanks for reading and hope this helped you understand what promises are and how to use them to simplify your asynchronous code but if you have any questions or even additions please feel free to leave a comment below.

--

--

Lexical Magazine
The Startup

Lexical is a software development media company sharing the knowledge and wisdom we have acquired over the years with the community.