Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Using Promises with JavaScript

In this hands-on lab, you'll learn how to create and consume promises, which are a way to eliminate passing around callbacks and write code that is able to express what is happening behind the scenes. You'll learn about the three states of each promise: pending, fulfilled (or resolved), and rejected. You'll also create a promise to replace the callback for an HTTP request. By the end of the lab, you'll also use axios, a promise-based HTTP client for JavaScript, to consume a promise.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 12m
Published
Clock icon Sep 12, 2022

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Promises are a way to eliminate passing around callbacks and write code that is able to express what is happening behind the scenes.

    Every promise has one of 3 states:

    1. pending - This is the initial state
    2. fulfilled or resolved - The operation completed successfully
    3. rejected - The operation failed

    HTTP calls are great opportunities to use promises. While the request is executing, the promise will be in the pending state. If the request succeeds (i.e. sends back a 200 code), then the promise will be fulfilled. If the request fails (e.g. a 404 or 500 error), then the promise will be rejected.

    In this course, you'll start by creating a promise that we'll use to replace the callback for an HTTP request.

    Once you've learned how to create promises, you'll move on to consuming them. You'll do that by using axios, which is a promise based HTTP client for JavaScript that follows the same rules as above. That is, when an HTTP call is successful, axios will resolve the promise. When an HTTP call is unsuccessful, for example, when it returns a 404, axios will reject the promise.

    And because each axios function returns a promise, you are able to chain multiple calls together.

    We'll cover each of these examples, and more, in the following steps.

  2. Challenge

    Create and resolve a promise

    Open the created.promises.js file, which we'll use for the first two steps. Notice the customPromise function. It uses the node module, http to perform a get request to get an orderStatus by its id.

    This code is asynchronous. That is, http.get will not return the data, instead uses a callback (the cb variable) to send either a successful message

    res.on('data', (chunk) => {
    		cb(JSON.parse(chunk));
    });
    

    Or handle the case where the call is not successful on line 11.

    cb(res.statusCode);
    

    Instead of using a callback, we'd like this function to use a promise so that the consumers don't have to pass around a callback.

    Promises are plain JavaScript objects. As a result, the first step in using a promise is creating a new promise. The constructor takes a function, and the first parameter of that function is another function most commonly named resolve

    new Promise((resolve) => {})
    ``` Now that you've created a promise, it's time to tell the promise to resolve. Use the `resolve` function that is passed in to the constructor.
  3. Challenge

    Create and reject a promise

    Just like a promise can be resolved, it can also be rejected. This is how you will communicate to your consumers that the promise failed.

    As you've already seen, when you create a promise, you pass in a function that takes a parameter commonly named resolve

    new Promise((resolve) => {});
    

    The function can also take a second parameter commonly called reject.

    new Promise((resolve, reject) => {})
    
  4. Challenge

    Handle a resolved promise

    Now that you've created a promise and been responsible for either resolving or rejecting it, it's time to move on to the more common use case: consuming promises.

    To start, navigate to the file http.promises.js. We'll use this file for the next several steps.

    Start with the function getOrders. This function uses axios get a list of available orders in our e-commerce system.

    Since axios.get returns a promise, the code on line 6 is incomplete. This line

    	return axios.get('http://localhost:8888/orders');
    

    does not return the orders, but rather a promise that will eventually resolve with orders.

    To have getOrders actually return the orders, we need to handle the case where the promise resolves.

    When a promise resolves it calls a function named then, which itself takes a function. For example:

     .then(result => {
     });
    

    One thing to know about axios is that the response that it returns contains more than just the data. It also contains meta-data such as the status code, url, and more. It stores the data in a property named data in that response.

  5. Challenge

    Chain resolved promises

    Promises can be chained together, this means that you can have successive then functions.

    That can be helpful, particularly when you need to make one call before you can make another call. For example, in our e-commerce application, we might fetch a specific order and then with that information, look up an additional piece of data.

    There's not really anything special with the syntax of chaining then functions. They'll look something like:

    .then(response => {})
    .then(response => {})
    

    There's also not really a limit on the number of promises you can chain together!

  6. Challenge

    Handle a rejected promise

    So far you've seen how promises work when everything goes right, but what about when there's an error?

    With HTTP requests, like we're doing with axios, an error could be an Internal Server Error (a 500 code), or even something like the 404 - Not Found status. It's important to know if your HTTP call succeeded.

    Promises help with this as well. Just like the then function, promises offer a second function, catch that will handle errors. Syntactically, it's very similar

    .catch(err => {});
    

    To test this out, make a call to get an order with an id of -1. That order doesn't exist, and so the server will return 404, and the axios promise will fail.

  7. Challenge

    Execute code after a promise is settled

    Oftentimes you will want to perform some kind of action after your promise runs regardless of if it succeeded or failed. For example, after making an HTTP call, you might want to turn off a "loading indicator" so that the user can continue using the application.

    With promises, this state is called settled. That is, the promise has executed and has either been fulfilled or rejected and so the promise is now in the settled state.

    You've already seen that promises use a .then function for the fulfilled result and a .catch for the rejected result. So it shouldn't be too surprising that there's a third function for this settled state. That function is named finally and it is used very similar to the other functions.

    	.finally(() => {});
    
  8. Challenge

    Wait for all calls to succeed

    Up until now you've been working with a single promise at a time, and that will be helpful in handling asynchronous code. However, one aspect of promises that are very powerful is the ability to queue up multiple promises so that they can run in parallel.

    Open up the multiple.promises.js file.

    Look at the function loadPartialMetadata(), and you can see three promises assigned to three variables on lines 4-6

        let categories = axios.get("http://localhost:8888/itemCategories");
        let statuses = axios.get("http://localhost:8888/orderStatuses");
        let userTypes = axios.get("http://localhost:8888/userTypes");
    

    Remember that axios returns promises. So categories, statuses and userTypes are not the data from the API, but rather promises that point to that call.

    Each of these API calls return metadata that your users will use throughout the app. There's no business case for why one of those API calls should wait on the others. That is itemCategories doesn't depend on orderStatuses.

    At the same time, it doesn't make sense to allow the program to continue until all of the calls have returned their data.

    The Promise object accounts for this with a variety of functions that can be used for queuing up promises.

    The first one to look at is Promise.all. This function accepts a list of promises and then will resolve when all promises resolve.

    	Promise.all([promise1, promise2, ... promiseN])
    ``` Since `Promise.all` creates a new promise, it will now behave the same as the other promises you've worked with. That means that when it `resolves` it will still call a `.then` function.
    
    One important piece to know is what the response data will be inside your `.then` function. `Promise.all` resolves with an array of data that is in the same order as the array it was called with.
    
    That is, in this code block:
    
    ```JavaScript
    	Promise.all([promise1, promise2])
    	.then(result => {})
    

    The variable result will be an array [response1, response2]

    The loadPartialMetadata needs to make a make sure that it loads itemCategories, orderStatuses, and userTypes. However, for performance reasons, it does not want to do so sequentially. It's important to note one thing about the behavior of .all. It will resolve when all of the promises have resolved. However, it will fail the instant that any one of the promises is rejected.

    For example, if categories succeeded, and orderStatuses failed, Promise.all would not wait around to see what userTypes would do, it would immediately reject the promise and your .then block would not be called. You would need to handle that case by chaining a .catch function.

    However, in the next step, you'll learn about another function on the Promise object that can handle this case.

  9. Challenge

    Wait for all calls to settle

    Inside the multiple.promises.js file there's a second function named loadAllMetadata. This function looks very similar to the loadPartialMetadata function from the last step.

    However, there is one difference, and that is, there's a new metadata type, addressTypes.

    In the API, addressTypes do not exist. So if you tried to load that metadata you would get a 404 - Not Found response from the API.

    If you were to use Promise.all, like in the last step, you would never be able to load your metadata, because addressTypes would fail. While that might be useful in some cases, it's not always useful. Sometimes you would like to have whatever data you can, and move on, even if all the promises don't resolve.

    For that, there's the function allSettled. Like all, this function creates a new promise. This new promise will only resolve once all the promises that are passed in are settled (either resolved or rejected).

    The function is called in the same way as all, by passing in a list of promises.

    The data that is returned is slightly different, however. Like all the data will be returned in the same order it was called. Unlike all, though, each returned value will have a status property that will be either fulfilled or rejected

    The loadAllMetadata function needs to handle the case where addressTypes fails while itemCategories and orderStatuses succeeds.

  10. Challenge

    Putting It All Together

    Now it's time to take what you've learned in the previous steps and apply it to an application.

    Start by clicking the Run button in the bottom right.

    Then, in the Web Browser tab, refresh the browser and you'll see a store front for Carved Rock Fitness.

    Notice that there is a My Orders menu item. This item will show you the orders that you have in the system. But in order to get the orders to show up, you need to fetch some data.

    Start by opening the file store/modules/orders.

    This contains a function where you'll be writing your code.

    You'll notice at the top of the function, there are promises for every piece of data that you will need.

    let statuses = axios.get("http://localhost:8888/orderStatuses");
    let users = axios.get("http://localhost:8888/users");
    let items = axios.get("http://localhost:8888/items");
    let addresses = axios.get("http://localhost:8888/addresses");
    let orders = axios.get('http://localhost:8888/orders?userId=2');
    

    Notice that the call to orders specifies the userId. It's hard-coded to 2 for this example, but in a real world application, it would use the id of whatever user is logged in.

    It's important to note that this is not a good way to load data in a real application. It works in this situation because there is not much data and so connecting the data on the client will not have a performance impact. In a more real-world application, a request to orders would likely return the status, items and addresses as one object.

    To make the orders show up the following steps need to happen:

    1. Queue up all promises using Promise.all
    2. Set the metadata, such as statuses on the metadata object.
    3. Create an array of orders that contains the status, address, and list of items At this point, you've retrieved all of the metadata that you will need for the order, and you've passed the order data on to the next then block. In the Web Browser tab, if you refresh and click the My Orders menu item, you should 2 cards, one for each order.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.