Promises

Open the project folder

In our starter files, open the index.html page from this lessons folder:

10.Async-JavaScript > 03.Promises

Introduction

We know now that making an asynchronous request can take time. Maybe a few milliseconds, a few seconds, minutes, or never complete at all.

Often when we are talking about this, it refers to fetching something from outside our website or app. Possibly a server, data store, API, or an external website.

The Promise object

And a Promise is great for handling this outcome. A Promise is an object, and this object represents the result of an async task. The result could be a failure, we may not get what we need from the server or, it may take a little time.

Promises hold onto a task and promise to tell you in the future when the outcome is known. And we have already seen promises and some of these outcomes already.

Using with the Fetch API

Earlier we looked at a simple example of fetching an image from Wikimedia, using the Fetch API:

const imageSrc = fetch(
  'https://upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Kamenn%C3%A1_l%C3%BAka_03.JPG/2560px-Kamenn%C3%A1_l%C3%BAka_03.JPG'
);
console.log(imageSrc);

Fetch is an async operation, and we see a Promise in the console. When we tried to access the Promise too early, before it finished getting the image, it was pending. After adding a time delay with a timeout, the result was fulfilled.

Available states

And these are two of the three available states with promises:

  • Pending: initial state, no result yet.
  • Fulfilled: task completed successfully.
  • Rejected: task has failed.

Pending is the initial state when we don’t yet know if the task will complete or not. Then fulfilled when the task was deemed a success. Such as the data is returned and is ready to use.

Then we also have rejected for when the task has failed, we don’t have the data we requested, and we need to do something about it.

📝 You may also hear the word settled when using promises, settled is when we know the fate of the task, meaning it is either fulfilled or rejected.

We don’t need to do anything about the pending state, since we can’t tell what direction it will go with the result. But we do need to handle a success or failure.

Example

Over in the starter file we see a Fetch API example:

fetch('https://dog.ceo/api/breed/image/random');

This is fetching data from an external API. This is an external URL which selects a random dog image.

Copy this URL (without quotes), and paste it into the browser:

{
  "message": "https://images.dog.ceo/breeds/otterhound/n02091635_3941.jpg",
  "status": "success"
}

JSON

This response is in a format called JSON, which stands for JavaScript Object Notation. JSON is a syntax for exchanging data over the web.

It looks like a JavaScript Object, but is language independent, meaning it can be created and parsed by other languages too. Although it is structured similar to a JavaScript object, data is transferred as plain text.

The result is a random jpg image from the API. We access it with the message property. Also, a status of success.

The then method

As we know, fetch is async. We can handle the outcome of fetching this image with a Promise.

First, we will handle the fulfilled state, and we do this by chaining the then method onto the fetch:

fetch('https://dog.ceo/api/breed/image/random')
  .then()

📝 When chaining the then() method make sure the semi-colon is removed from the end of the fetch line.

Fetch returns a Promise as the response, and therefore we have access to this promise method called then.

Next we pass in a callback function to run if this was a success:

fetch('https://dog.ceo/api/breed/image/random')
  .then(function () {
  })

This is an async callback and we only run this code once the task was a success.

Of course, we want to access the data returned from fetch, and this can be passed to the function:

.then(function (response) {
    console.log(response);
})

The Response object

You should see the following in the console:

Response {type: "cors", url: "https://dog.ceo/api/breeds/image/random", redirected: false, status: 200, ok: true, …}
    body: ReadableStream
    bodyUsed: false
    headers: Headers {}
    ok: true
    redirected: false
    status: 200
    statusText: ""
    type: "cors"
    url: "https://dog.ceo/api/breeds/image/random"
    [[Prototype]]: Response

This successful promise returns a Response object. This object contains data such as the status code, 200 means a success, and we see a URL.

You may notice the URL is not the one we want. This is the URL used to ask for the random image, not the jpg URL we had earlier in the browser.

This goes back to the JSON format. The data we need is stored inside the body property:

body: ReadableStream

The json() method

And to read this JSON data, this response body object has a json() method available:

fetch('https://dog.ceo/api/breeds/image/random')
.then(function (response) {
  console.log(response.json());
});

Resulting in the following in the console:

Promise {<pending>}

We are back to a pending Promise. The task has not yet completed. This is because the json() method also returns a Promise which we need to handle.

There are a couple of ways we can do this. First, remove the console.log wrapper:

fetch('https://dog.ceo/api/breeds/image/random')
.then(function (response) {
  response.json()
});

And since the json method returns a Promise, we can again chain onto the end the then method:

fetch('https://dog.ceo/api/breeds/image/random')
.then(function (response) {
  response.json()
  // also receives in data from the response
  .then(function(data) {
    console.log(data);
  })
});

This is the same URL in the message property we seen when we pasted the link in the browser.

message: "https://images.dog.ceo/breeds/pointer-german/n02100236_2119.jpg"
status: "success"

Everything here returns a Promise. The fetch call, and the json method.

Alternative syntax

There is also an alternative syntax to avoid nesting the then methods like above. We can keep them all at the top level to help readability.

To do this, we can refactor as follows:

fetch('https://dog.ceo/api/breeds/image/random')
  .then(function (response) {
    // return json data
    return response.json();
  })
  // chain the then method to the end:
  .then(function (data) {
    console.log(data);
  });

We can chain as many of these then methods as we need to, each one waiting on the previous result before running. Keeping full control over our async tasks and the order in which they run.

Displaying the image in the browser

The data.message property gives us the URL we need for our image. And we can use it to display in the browser:

<body>
  <!-- 1. add empty image element -->
  <img src="" alt="" />
  <script>
    fetch("https://dog.ceo/api/breeds/image/random")
      .then(function (response) {
        return response.json();
      })
      .then(function (data) {
        // 2. set image src
        document.querySelector("img").src = data.message;
        console.log(data.message);
      });
  </script>

Refresh the browser to see random dog images!

The catch method

This is the fulfilled state handled, what about the rejected state? This happens if there is an error fetching the data or completing the task we set.

For this, we chain the catch method to the end:

fetch('https://dog.ceo/api/breeds/image/random')
  .then(function (response) {
    return response.json();
  })
  .then(function (data) {
    document.querySelector('img').src = data.message;
    console.log(data.message);
  })
  .catch()

📝 Remember, when chaining any method, the semi-colon must be removed from the previous line.

This will run when there is an error, and pass that error to a callback function:

.catch(function (error) {
  console.log("There was an error: " + error);
});

If there are no errors, we won’t see anything. To test this, we can switch off the network in the browser developer tools.

In Chrome, there will be an option in the network tab:

Chrome network tab

Other browsers will have similar options. This dropdown has an option to set the network to be offline. Switching off the network means we cannot get the image we want, and the Promise should throw an error in the console:

⚠️ There was an error: TypeError: Failed to fetch

The finally method

The last method we will cover is finally. The finally method will run regardless of if the Promise was fulfilled or rejected.

And this can also be chained too:

// ...
.catch(function (error) {
  console.log('There was an error: ' + error);
})
.finally(function () {
  console.log('I will always run');
});

The code in here will always run. You can again test this with the network settings. The finally method is useful to follow up with some code to run after the promise has settled. The alternative would be to place this code into both the then and the catch sections, resulting in duplicated code.

This example was possible because the fetch method initially returns a promise to start the whole chain. But what about things that don’t return a promise? Next, we will look at how we can handle this.