JavaScript Promises: Why and How

JavaScript Bytes Series

ยท

10 min read

JavaScript Promises: Why and How

If you don't learn async programming you'll likely leave yourself with a lot of broken Promises. ๐Ÿฅ

What are Promises?

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.1

So what does that all mean? Promises simply give us an easier way to work with asynchronous requests and responses. Let's compare a similar practice of managing async calls but with an older technique called a "callback."

What are callbacks?

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.2

A callback is simply a method signature that you pass in to another function as a variable. You can then pass the data retrieved in the current method back to another method for further handling.

Why is this all useful?

Promises and Callbacks become extremely useful in async programming because we often face the issue of timing. You don't know for sure when an asynchronous request is going to finish (gee, thanks captain obvious). If you have a future async call for data that is dependent upon a previous async request you could have a timing issue that results in data being loaded improperly, or not at all. Now, you may be thinking if I'm using async functions then why in the world am I acting like it's a synchronous operation? Well, most API requests rely on asynchronous calls simply because the separation between the front and back end systems. You have no choice really but to use async requests. And if you're relying on getting some value back before moving on then technically it has become synchronous. Enough about that though, that's just getting into the weeds.

How to Use Callbacks

Let's start with a classic callback way of handling an asynchronous function. We're going to spoof an async request using the setTimeout() function, but just pretend it's an AJAX request to our API.

function getMyFirstName(callback)
{
    // Here we make an initial async request to get the First Name
    setTimeout(() =>
    {
        var firstName = "Charles";
        callback(firstName); // How to handle the data once it's returned
    }, 3000); // Run after 3 seconds    
}

function sayHello(name)
{
    alert("Hello, " + name + "!");
}

getMyFirstName(sayHello);

Great, we were able to manage the result of an async call so what's the problem? Let's take a look at the example above but with a few minor tweaks. We're going to add yet another callback to our initial callback method so we can "chain" together our functions.

function getMyFirstName(callback)
{
    // Here we make an initial async request to get the First Name
    setTimeout(() =>
    {
        var firstName = "Charles";
        callback(firstName, sayHello); // How to handle the data once it's returned
    }, 3000); // Run after 3 seconds    
}

function getMyLastName(firstName, callback)
{
    // Here we make a second async request to get the Last Name
    setTimeout(() =>
    {
        var lastName = "Jennings";
        callback(firstName + " " + lastName); // How to handle the data once it's returned
    }, 3000); // Run after 3 seconds    
}

function sayHello(fullName)
{
    alert("Hello, " + fullName + "!");
}

getMyFirstName(getMyLastName);

Now you should see the full name returned after 6 seconds. If you wrote this all yourself you probably already know why this is a potential problem. By only making two async function calls we already have a readability issue. If you continue to do it this way you can end up with a nightmarish amount of nested callbacks that become untidy and confusing to follow. Enter the Promise object with it's beautiful chaining method called .then()

How to Use Promises

Let's do this exact same example but instead of callbacks we'll implement the Promise object. Here's how that would look in the first example:

function getMyFirstName() {
    return new Promise((resolve, reject) => {
        // Here we make an initial async request to get the First Name
        setTimeout(() => {
            var firstName = "Charles";

            resolve(firstName);
        }, 3000);
    });
}

getMyFirstName().then((firstName) =>
{
    alert("Hello, " + firstName + "!");
});

A few important parts here are of course initializing the new Promise() instance. Inside that you have a lambda expression containing the word "resolve." Think of that as the exact same as the word "callback" from above. Using the "resolve" keyword here is common practice with Promises, but it could be named anything you'd like. There is also a "reject" parameter which is used to feed the .catch() method of a Promise. By using the reject you could do error handling if say your API call failed completely.

The resolve(firstName) takes the async return value and passes it on to the .then() method to use later. Next, let's chain this into the full name version like in the first example. Here is what it would look like chaining two async calls together:

function getMyFirstName() {
    return new Promise((resolve, reject) => {
        // Here we make an initial async request to get the First Name
        setTimeout(() => {
            var firstName = "Charles";

            resolve(firstName);
        }, 3000);
    });
}

function getMyLastName(firstName) {
    return new Promise((resolve, reject) => {
        // Here we make an initial async request to get the First Name
        setTimeout(() => {
            var lastName = "Jennings";

            resolve(firstName + " " + lastName);
        }, 3000);
    });
}

getMyFirstName()
    .then((firstName) => getMyLastName(firstName)
        .then((fullName) => {
            alert("Hello, " + fullName + "!");
        }));

As you can see in the last section where you call to the getMyFirstName() function you "chain" the next function to the lambda expression of the first. The values you're capturing in the left side of the lambda expression are the result of the resolve() method being called inside of the Promise object constructor.

A Different Approach to Chaining

Let's say you didn't need to rely on another async call after your initial async call. You can chain the .then() methods together into something like the following if you ever needed to:

function getMyNumber() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            var startingNumber = 1;

            resolve(startingNumber);
        }, 3000);
    });
}

getMyNumber()
    .then((number1) =>
    {
        return number1 + 1;
    }).then((number2) =>
    {
        return number2 + 1;
    }).then((finalNumber) =>
    {
        alert(finalNumber);
    });

This way of handling Promises flattens the response chain and allows you to continue the calculations instead of having a nested chain. The value you pick up in the lambda expression is the returned value of the previous .then() method.

Summary

The examples above are basically useless in terms of why in the world would you ever need to make two separate async calls just to get someone's first and last name and then put them together. My hope is that this was easier to understand than an overly complex example. Promises take some getting used to but once you do they are far easier to work with and maintain than callbacks. If you only need to do a single callback after a method then by all means callbacks are great. If you are doing a lot of async calls that depend on some other async call then Promises are probably what you'll want to use.


Additional Reading


  1. developer.mozilla.org/en-US/docs/Web/JavaSc..
  2. developer.mozilla.org/en-US/docs/Glossary/C..

Did you find this article valuable?

Support Charles J by becoming a sponsor. Any amount is appreciated!

ย