Promises

Promise

Asynchronous programming in modern JavaScript revolves around promises. A Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. A Promise object can be created using the Promise constructor. The constructor takes an argument being the executor function ((resolve, reject) => {...}) that handles the asynchronous operation, i.e., the "event". The details about the executor will be explained further in this section.

After the asynchronous operation completed (or failed) an event handler is called to handle the result of the asynchronous event. This is taken care of by a then() method, which will be explained in the next section.


const myPromise = new Promise((resolve, reject) => {
  // do something asynchronous which eventually calls either
  // resolve(someValue) or reject("failure reason"):
  const img = document.createElement('img');
  img.src = "https://art.fridoverweij.com/images/Girl_with_a_Pearl_Earring-420px.jpg";
  img.onload = () => resolve(img);
  img.onerror = () => reject(new Error("Image loading failed"));  
});

The executor generally does something asynchronous but this is not mandatory: resolve or reject can also be called immediately.

Earlier we used a general loadImg function that we can pass to a given image to load. We can adjust this function so that it returns the promise.


function loadImg(src) {
  return new Promise((resolve, reject) => {
    const img = document.createElement('img');
    img.src = src;
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Image loading failed for ${src}`));  
  });
};  

Creating a promise automatically invokes the executor function. The executor starts the event, i.e., the asynchronous operation, loading the image (by initializing the src attribute) in the example above. When the promise is created it also automatically generates two functions: a resolution function and a rejection function. These two functions should be passed into the executor. In the example above they are given the names resolve and reject. You need to have the executer function call the resolution function in case the event finished successfully and call the rejection function in case the event failed.

The initial "state" of the promise is "pending" until eventually it is settled. When the promise was settled its state changed to either fulfilled (aka resolved) or rejected. On fulfillment, the executer calls the resolution function, on rejection, the executer calls the rejection function. Either the (first) resolution function or the (first) rejection function is called, any possible further call of a resolution or rejection function will be ignored.

The resolution function and the rejection function both accept a single argument of any type. Typically the result of the event (or part of it) is passed to the resolution function. Typically a reason for the failure, through an error object, is passed to the rejection function.

then() & event handlers

We are still missing the event handlers. The event handlers are defined in the then() method of the promise.


myPromise.then(resolutionHandler, rejectionHandler);

PS: The rejection handler is optional.

The resolution function or rejection function in the executer calls its corresponding event handler in the then() method, while passing its arguments. In the example below, argument img in resolve(img) will be passed to parameter result of the first event handler, and the error object argument of the rejection function will be passed to parameter error of the second event handler.


const myPromise = new Promise((resolve, reject) => {
  const img = document.createElement('img');
  img.src = "images/Girl_with_a_Pearl_Earring-420px.jpg";
  img.onload = () => resolve(img);
  img.onerror = () => reject(new Error("Image loading failed"));  
}).then(
  (result) => { console.log(`Image size: ${result.width}x${result.height}`) },
  (error) => { console.log(error) }
);
// logs: "Image size: 420x560"

Or with a general loadImg function:


function loadImg(src) {
  return new Promise((resolve, reject) => {
    const img = document.createElement('img');
    img.src = src;
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Image loading failed for &{src}`));  
  });
};

const imagePromise = loadImg("images/Girl_with_a_Pearl_Earring-420px.jpg");

imagePromise.then(
  (result) => { console.log(`Image size: ${result.width}x${result.height}`) },
  (error) => { console.log(error) }
);
// logs: "Image size: 420x560"

imagePromise.then(
  (result) => { document.getElementById('imgcontainer').append(result); },
  (error) => { console.log(error) }
);

In the above example we called the then() method twice on the same promise, i.e., multiple event handlers on the same loaded image. This is not possible with only using callbacks. With the asynchronous callback-based approach, as covered in the previous chapter, we loaded the image and called the event handler(s) (via the callback) in the same loadImg function.

fetch()

In the image loading examples above we (implicitly, by assigning the src attribute) used an HTTP requests for the image using the XMLHttpRequest API. The asynchronous .onload and .onerror events are part of this API.

A modern, more powerful and flexible way to request a resource (i.e. download a file) than using XMLHttpRequest is using the fetch() global method. The fetch() method returns a Promise object. The resolution function of this promise passes a Response object, in other words: the promise resolves with a Response object. The Response object is an interface of the Fetch API.


fetch("https://www.somesite.com/some_resource")
  .then(
    (response) => {
     // body of the resolution event handler
  });

The rejection function will not be called on a HTTP error status (e.g. 404 file not found or 500 Internal Server Error). However, the response.ok property will be set to false. The promise will only reject if there was no response at all or if there was an "empty" response (no headers).


fetch("https://www.somesite.com/some_resource")
  .then(
    (response) => {
      if (!response.ok) {
        throw new Error('Error in network response');
      }
     // rest of the body of the resolution event handler
    },
	(error) => { console.log(error) }
  );

Extracting the resource from the Response object

The Response object does not directly contain the actual requested file but is instead a representation of the entire HTTP response. Generally, the actual resource needs to be extracted from the body content from the Response object. This extraction is also asynchronous.

In the next example below, the JSON body content is extracted from the downloaded JSON file (using response.json()). Extracting content from a plain text file or from an HTML or SVG file can be done by using response.text(). In the example further down below an actual image is extracted from the response body data of a downloaded image.

Chained promises

The then method itself returns a promise which resolves with the result of its resolution handler, or reject with the result of the rejection handler. If the resolution handler throws an error, the promise will reject with this error. This allows us to chain a number of successive asynchronous functions.

The next example downloads a JSON file. Method response.json() is used to extract the JSON body content from the Response object. Method response.json() returns a promise which resolves with the result of parsing the response body text as JSON. The promise returned by the first then resolves with the promise returned from response.json(), that, in turn, resolves with the result of parsing the response body text as JSON (data in the example below). The second then calls a function on that promise (data) returned by the first then


fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
  .then((response) => response.json())
  .then((data) => console.log(data),
        (error) => console.error(error)
  );

If then has no resolution handler, the result will simply be passed through to the next then resolution handler. Likewise, if then has no rejection handler, the error will simply be passed through to the next then rejection handler. If, in the example above, the fetch throws an error, the error will be passed through the first then to the last then and handled there by the rejection handler. The next example does exactly the same as the previous example.


fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
  .then()
  .then((response) => response.json())
  .then()
  .then((data) => console.log(data),
        (error) => console.error(error)
  );

catch() & finally()

catch()

Because of this chaining of promises we can rewrite the above example as:


fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
  .then((response) => response.json())
  .then((data) => console.log(data))
  .then(undefined, (error) => console.error(error));

Any possible error in the chain rejects all next promises and is passed on to the last then. The then method requires a resolution handler, but in the last then we are only handling the possible error so it is set to undefined.

In line with the try...catch syntax we can replace the last then in the above example with a Promise.catch() method: .catch((error) => console.error(error)) will do exactly the same. The catch() method deals with rejected cases only. A catch() at the end of a promise chain will be called when any of the consecutive asynchronous operations failed. A complete code to download a JSON file:


fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(data[0].name);
  })
  .catch((error) => {
    console.error(error);
  });
// logs: "baked beans"  

Note that a possible error thrown in the first then will also bubble down to the catch().

Compared to the nested callbacks, the "pyramid of doom" from the previous chapter, we now have a single place to handle all errors and no ever-increasing levels of indentation.

Also the catch method returns a promise which resolves with the result of the resolution handler (undefined) or rejects with the returned error handling (which happens when something goes wrong with the error handling itself). So we might continue the promise chain with a new then after a catch: the process continues, only after possible previous errors have been handled by the catch handler.

finally()

Suppose we add another then method after the catch. This final then we give two equal callbacks. Then this last then's handler will always be executed.


let isLoading = true;
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
  .then((response) => response.json())
  .then((data) => console.log(data[0].name))
  .catch((error) => console.error(error))
  .then(
    () => {isLoading = false; console.log(isLoading)},
	() => {isLoading = false; console.log(isLoading)}
	);
// logs:
// "baked beans"
// false // even if an error occurred, which is correct; the loading did stop, either successfully or with an error.

In line with the try...catch...finally syntax we can replace the last then in the above example with a Promise.finally() method:


let isLoading = true;
fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json')
  .then((response) => response.json())
  .then((data) => console.log(data[0].name))
  .catch((error) => console.error(error))
  .finally(() => {isLoading = false; console.log(isLoading)});
// logs:
// "baked beans"
// false

However, then with two equal callbacks and a finally do not act exactly the same:

Example - Downloading an image

Previously we implicitly downloaded, that is, copied an image from some (remote) server and "cached" it (stored it inside the browser), by assigning a URL to the img's src attribute. We used the asynchronous onload to notify the program that loading finished and to provide a function to do something with the result.


img.src = "images/Girl_with_a_Pearl_Earring-420px.jpg";
img.onload = () => {
  console.log(`Image size: ${img.width}x${img.height}`); // logs: "Image size: 420x560"
}

The fetch() method returns a Promise object that resolves with a Response object that is a representation of the entire HTTP response. We cannot assign that response to the img's src attribute. The actual image first needs to be extracted from the response body data (using response.blob()) and a temporary internal URL that points to this extraction stored inside the browser needs to be assigned to the img's src attribute (using URL.createObjectURL()).


const img = new Image();

fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((myBlob) => {
    img.src = URL.createObjectURL(myBlob);
    document.querySelector('body').append(img);
    URL.revokeObjectURL(img.src);
  })
  .catch((error) => {
    console.error('Something went wrong when loading the image:', error);
  });

The response body data of a fetched image is of a BLOB data type. Method response.blob() returns a promise which resolves with the result of parsing the response body data as a Blob. This Blob needs to be assigned to the img's src attribute as a URL. Method URL.createObjectURL() creates an Object URL, which is a unique, internal URL that only exists within the current document and only while the document is open. Once the image is appended to the document and the Object URL and associated Blob are no longer needed, the Object URL is released using URL.revokeObjectURL() for optimum memory management.

However, assigning an Object URL to the img's src attribute still triggers an asynchronous load, although it is a very fast one from local cache. Retrieving the image's dimensions using img.width and img.height will not work unless it is done within a onload callback function.


const img = new Image();

fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((myBlob) => {
    img.src = URL.createObjectURL(myBlob);
	document.querySelector('body').append(img);
    img.onload = () => { 
      console.log(`Image size: ${img.width}x${img.height}`);
      URL.revokeObjectURL(img.src);
    }
  })
  .catch((error) => {
    console.error('Something went wrong when loading the image:', error);
  });
// logs: "Image size: 700x658"  

An other way is to use global method createImageBitmap() that returns a Promise which resolves to an object that represents the image as a bitmap which can be drawn to a HTML <canvas>. This object has two properties: width and height (both return in CSS pixels).


const img = new Image();

fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((myBlob) => {
    img.src = URL.createObjectURL(myBlob);
    document.querySelector('body').append(img);
    return createImageBitmap(myBlob);
  })
  .then((ImageBitmap) => {    
    console.log(`Image size: ${ImageBitmap.width}x${ImageBitmap.height}`);	 
  })
  .catch((error) => {
    console.error('Something went wrong when loading the image:', error);
  })
  .finally(() => URL.revokeObjectURL(img.src));
// logs: "Image size: 700x658"

Return inside an asynchronous function

Suppose we want to do something asynchronous with the image after the onload and for that we want to pass the image to the next then by returning it:


const img = new Image();

fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((myBlob) => {
    img.src = URL.createObjectURL(myBlob);
    img.onload = () => { 
      return img;      
    }
  })
  .then((image) => {
    console.log(`Image size: ${image.width}x${image.height}`);
  })  
  .catch((error) => {
    console.error('Something went wrong when loading the image:', error);
  })
  .finally(() => URL.revokeObjectURL(img.src));
// logs: "Something went wrong when loading the image: TypeError: image is undefined" 

This does not work because the image is returned from the onload callback into the resolution handler and not returned by the resolution handler itself to the next then. We can fix this by returning the image by a resolution function of a new promise returned by the resolution handler:


const img = new Image();

fetch("https://mdn.github.io/dom-examples/fetch/fetch-request/flowers.jpg")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((myBlob) => new Promise((resolve, reject) => {
    img.src = URL.createObjectURL(myBlob);
    img.onload = () => {
      resolve(img);      
    }
  }))
  .then((image) => {
    console.log(`Image size: ${image.width}x${image.height}`);
  }) 
  .catch((error) => {
    console.error('Something went wrong when loading the image:', error);
  })
  .finally(() => URL.revokeObjectURL(img.src));
// logs: "Image size: 700x658" 

Combining promises

A promise chain handles multiple asynchronous functions that depend on each other and each one needs to complete before the next starts. Sometimes multiple promises that do not depend on each other need to be handled by the same resolution and rejection handlers.

The Promise API provides a few methods to handle sets of promises. One of them is the Promise.all() method. Promise.all() takes an array of promises and returns a single promise that resolves when all the promises in the array are resolved and it resolves with an array of all the responses in the same order as the promises in the array. The returned promise rejects when any of the promises in the array is rejected and it rejects with the error thrown by the promise that rejected.

In the next example the second resource does not exist. The server returns 404 (Not Found) instead of 200 (OK).


Promise.all([
  fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json'),
  fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found'),
  fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json')
])
  .then((responses) => {
    for (const response of responses) {
      if (!response.ok) {
        throw new Error(`Error in network response: ${response.status} for fetch ${response.url}`);
      }      
    }
    return responses
  })
  .then((responses) => {
    for (const response of responses) {  
      console.log(`${response.url}`);
    }
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`)
  });
// logs: "Failed to fetch: Error: Error in network response: 404 for fetch https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found"

Promise.any() is similar to Promise.all() except that the returned promise resolves as soon as any of the array of promises is resolved, or rejects when all of the array of promises are rejected. Promise.race() returns a promise which resolves as soon as any of the array of promises is either resolved or rejected.