Introducing and Understanding JavaScript Promises

A promise allows us to delay the execution of code that's dependent upon information retrieved by an asynchronous operation. We'll expand on what this means, how to use promises, and how to go about creating one.

Published
Aug 05, 20

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

JavaScript is a single-threaded language, meaning it can only execute tasks sequentially one after another. It cannot multitask by running two blocks of code at the same time. Promises leverage asynchronous operations to allow us to manipulate the order in which JavaScript sequentially runs through it's tasks. This can be a tricky concept to grasp, so my goal here is to make it as beginner-friendly as possible.

Understanding Promises

A promise, as defined by the MDN Web Docs, "is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future."

In other words, a promise allows us to define a block of code that won't run until an asynchronous operation is fulfilled or rejected. This is especially useful when that block of code relies on information being retrieved and returned from the asynchronous operation.

For example, say we need to retrieve information from our database to display on our site. To do this, we'd use an API call to our server that will grab the information then return it back to us. It looks something like the below.

// call fetch, our asynchronous operation, to retrieve data from server
const ourPromise = fetch('https://pipl.ir/v1/getPerson');

// log out our promise
console.log(ourPromise);
Copied!

Here we use fetch to communicate with our API server to grab some information. Our fetch call returns a promise, which we then store in a constant variable called ourPromise. However, we currently aren't doing anything with our promise. This takes us into promise states.

Promise States

Once a promise is created it receives a pending state, which is the initial state, meaning our promise has neither succeeded nor failed. Once the promise finishes, which means it ran and either succeeded or failed, it will get one of two states. The fulfilled state is what we get back if everything is hunky-dory and the promise completed successfully. The rejected state is what we get if somewhere along the line the promise failed.

We can use these states to appropriately handle our promise, whether it was fulfilled or rejected, like so:

// call fetch, our asynchronous operation, to retrieve data from server
const ourPromise = fetch('https://pipl.ir/v1/getPerson');

// bind fulfilled handler to the promise
ourPromise.then(function fulfilledHandler(returnedData) {
    console.log(returnedData);
}

// bind rejected handler to the promise
ourPromise.catch(function rejectedHandler(error) {
    console.log(error);
}
Copied!

Alternatively, we can use just the .then method to handle both fulfilled and rejected responses, as shown below.

const ourPromise = fetch('https://pipl.ir/v1/getPerson');

ourPromise.then(function fulfilledHandler(returnedData) {
    console.log(returnedData);
}, function rejectedHandler(error) {
    console.log(error);
});
Copied!

Note that you could also attach the .then and .catch methods directly to the fetch using method chaining.

If our promise is fulfilled, our fulfilledHandler will be called and our rejectedHandler won't be called. If our promise is rejected the opposite will happen, rejectedHandler will be called and fulfilledHandler won't be called.

Creating A Promise

You can also create a promise yourself. This has many uses, but to give an example we can create a promise ourselves to combine multiple API calls that rely upon one another to fulfill successfully. Below is an example of what creating a promise looks like.

const ourPromise = new Promise(function (resolve, reject) {
    const pretendData1 = 'abc123';
    const pretendData2 = 'def456';
	
    if (pretendData1 && pretendData2) {
        resolve({ pretendData1, pretendData2 });
    } else {
        reject('All or some required information could not be found');
    }
});

ourPromise.then(function fulfilledHandler(pretendData) {
    console.log(pretendData);
}).catch(function rejectedHandler(error) {
    console.log(error);
});
Copied!

We create a new promise, which accepts a callback function that's provided resolve and reject parameters. Both of these parameters themselves are also callback functions that, when called, inform our promise to execute the fulfilledHandler if the promise is resolved, and the rejectedHandler if the promise is rejected.

Real-World Example

Let's drive this concept home by walking through a use-case to something in the real-world. Imagine you're standing in line at the DMV (fun fun) and there is only one lane open, because JavaScript is single-threaded and only has one lane. You finally reach the representative and realize you need your license plate number but you don't have that information stored in your head yet.

There's two options on how to proceed.

  1. The representative and everyone in line can wait for you to run out to your car, get your license plate number, return back to the representative, and provide the required license plate number.

  2. You can excuse your position in line, grab your license plate number, return to the back of the line, and wait for your turn with the representative again.

Option 1 would keep the representative's customers waiting (your site's visitors). Option 2 would allow customers (your site's visitors) to continue with their business.

Option 1 is what is called a synchronous operation because you must finish your task by grabbing your license plate number before any of the other tasks can be done.

Option 2 is what is called an asynchronous operation because you step aside and allow other tasks to complete while you grab the missing required information, your license plate number. Then you return once you have that information to complete your task.

In option 2, when you excuse your position in line, you're promising the representative that you'll return with your license plate number. When you actually do return with your license plate number you're fulfilling your promise. If you fail to find your license plate number, maybe you brought the wrong car with you, then the promise is rejected.

Let's try to translate this to code below.

function fetchLicensePlate() {
    // create a promise returning our plateNumber after 5s
    return new Promise(function (resolve, reject) {
        var plateNumber = "JS2020";

        // timeout to pass some time
        setTimeout(function() { 
            // If I found the license plate number, resolve
            if (plateNumber) {
                resolve(plateNumber);
            } else {
                reject("Plate number could not be found");
            }
        }, 5000);
    });
}

console.log('before fetchLicensePlate');

// when we call a function that returns a promise, we can chain directly off of it.
fetchLicensePlate()
    .then(function(plateNumber) {
        // thank goodness we brought the right car!
        console.log(plateNumber);
    })
    .catch(function(error) {
        // we brought the wrong car :(
        console.log(error);
    });

console.log('after fetchLicensePlate');
Copied!

First we define our function fetchLicensePlate. Within this function we create and return a new promise. Inside our new promise we have a setTimeout. This is to mock the behavior of an asynchronous call. Since we define plateNumber within our promise, when we look for it in our setTimeout our if statement will be truthy and our plateNumber will be resolved.

Since we're returning a promise from our fetchLicensePlate function we can chain our promise handlers directly off the call of our function.

Next Steps

To fully understand promises it's best to just play around with them. I'd recommend using a free already made API endpoint, like I did in the non-real world examples in this post. You can find a list of free API endpoints here. Once you feel you're beginning to get comfortable with promises using the callback syntax we used here, you can then begin to look into the async/await syntax. This syntax abstracts away the asynchronous nature of promises so they more closely resemble working with synchronous code.

Join The Discussion! (0 Comments)

Please sign in or sign up for free to join in on the dicussion.

robot comment bubble

Be the first to Comment!