JavaScript for impatient programmers (ES2022 edition)
Please support this book: buy it or donate
(Ad, please don’t block.)

40 Promises for asynchronous programming [ES6]



  Recommended reading

This chapter builds on the previous chapter with background on asynchronous programming in JavaScript.

40.1 The basics of using Promises

Promises are a technique for delivering results asynchronously.

40.1.1 Using a Promise-based function

The following code is an example of using the Promise-based function addAsync() (whose implementation is shown soon):

addAsync(3, 4)
  .then(result => { // success
    assert.equal(result, 7);
  })
  .catch(error => { // failure
    assert.fail(error);
  });

Promises are similar to the event pattern: There is an object (a Promise), where we register callbacks:

A Promise-based function returns a Promise and sends it a result or an error (if and when it is done). The Promise passes it on to the relevant callbacks.

In contrast to the event pattern, Promises are optimized for one-off results:

40.1.2 What is a Promise?

What is a Promise? There are two ways of looking at it:

40.1.3 Implementing a Promise-based function

This is an implementation of a Promise-based function that adds two numbers x and y:

function addAsync(x, y) {
  return new Promise(
    (resolve, reject) => { // (A)
      if (x === undefined || y === undefined) {
        reject(new Error('Must provide two parameters'));
      } else {
        resolve(x + y);
      }
    });
}

addAsync() immediately invokes the Promise constructor. The actual implementation of that function resides in the callback that is passed to that constructor (line A). That callback is provided with two functions:

40.1.4 States of Promises

Figure 22: A Promise can be in either one of three states: pending, fulfilled, or rejected. If a Promise is in a final (non-pending) state, it is called settled.

Fig. 22 depicts the three states a Promise can be in. Promises specialize in one-off results and protect us against race conditions (registering too early or too late):

Additionally, once a Promise is settled, its state and settlement value can’t change anymore. That helps make code predictable and enforces the one-off nature of Promises.

  Some Promises are never settled

It is possible that a Promise is never settled. For example:

new Promise(() => {})

40.1.5 Promise.resolve(): create a Promise fulfilled with a given value

Promise.resolve(x) creates a Promise that is fulfilled with the value x:

Promise.resolve(123)
  .then(x => {
    assert.equal(x, 123);
  });

If the parameter is already a Promise, it is returned unchanged:

const abcPromise = Promise.resolve('abc');
assert.equal(
  Promise.resolve(abcPromise),
  abcPromise);

Therefore, given an arbitrary value x, we can use Promise.resolve(x) to ensure we have a Promise.

Note that the name is resolve, not fulfill, because .resolve() returns a rejected Promise if its Parameter is a rejected Promise.

40.1.6 Promise.reject(): create a Promise rejected with a given value

Promise.reject(err) creates a Promise that is rejected with the value err:

const myError = new Error('My error!');
Promise.reject(myError)
  .catch(err => {
    assert.equal(err, myError);
  });

40.1.7 Returning and throwing in .then() callbacks

.then() handles Promise fulfillments. It also returns a fresh Promise. How that Promise is settled depends on what happens inside the callback. Let’s look at three common cases.

40.1.7.1 Returning a non-Promise value

First, the callback can return a non-Promise value (line A). Consequently, the Promise returned by .then() is fulfilled with that value (as checked in line B):

Promise.resolve('abc')
  .then(str => {
    return str + str; // (A)
  })
  .then(str2 => {
    assert.equal(str2, 'abcabc'); // (B)
  });
40.1.7.2 Returning a Promise

Second, the callback can return a Promise p (line A). Consequently, p “becomes” what .then() returns. In other words: the Promise that .then() has already returned is effectively replaced by p.

Promise.resolve('abc')
  .then(str => {
    return Promise.resolve(123); // (A)
  })
  .then(num => {
    assert.equal(num, 123);
  });

Why is that useful? We can return the result of a Promise-based operation and process its fulfillment value via a “flat” (non-nested) .then(). Compare:

// Flat
asyncFunc1()
  .then(result1 => {
    /*···*/
    return asyncFunc2();
  })
  .then(result2 => {
    /*···*/
  });

// Nested
asyncFunc1()
  .then(result1 => {
    /*···*/
    asyncFunc2()
    .then(result2 => {
      /*···*/
    });
  });
40.1.7.3 Throwing an exception

Third, the callback can throw an exception. Consequently, the Promise returned by .then() is rejected with that exception. That is, a synchronous error is converted into an asynchronous error.

const myError = new Error('My error!');
Promise.resolve('abc')
  .then(str => {
    throw myError;
  })
  .catch(err => {
    assert.equal(err, myError);
  });

40.1.8 .catch() and its callback

The difference between .then() and .catch() is that the latter is triggered by rejections, not fulfillments. However, both methods turn the actions of their callbacks into Promises in the same manner. For example, in the following code, the value returned by the .catch() callback in line A becomes a fulfillment value:

const err = new Error();

Promise.reject(err)
  .catch(e => {
    assert.equal(e, err);
    // Something went wrong, use a default value
    return 'default value'; // (A)
  })
  .then(str => {
    assert.equal(str, 'default value');
  });

40.1.9 Chaining method calls

.then() and .catch() always return Promises. That enables us to create arbitrary long chains of method calls:

function myAsyncFunc() {
  return asyncFunc1() // (A)
    .then(result1 => {
      // ···
      return asyncFunc2(); // a Promise
    })
    .then(result2 => {
      // ···
      return result2 ?? '(Empty)'; // not a Promise
    })
    .then(result3 => {
      // ···
      return asyncFunc4(); // a Promise
    });
}

Due to chaining, the return in line A returns the result of the last .then().

In a way, .then() is the asynchronous version of the synchronous semicolon:

We can also add .catch() into the mix and let it handle multiple error sources at the same time:

asyncFunc1()
  .then(result1 => {
    // ···
    return asyncFunction2();
  })
  .then(result2 => {
    // ···
  })
  .catch(error => {
    // Failure: handle errors of asyncFunc1(), asyncFunc2()
    // and any (sync) exceptions thrown in previous callbacks
  });

40.1.10 .finally() [ES2018]

The Promise method .finally() is often used as follows:

somePromise
  .then((result) => {
    // ···
  })
  .catch((error) => {
    // ···
  })
  .finally(() => {
    // ···
  })
;

The .finally() callback is always executed – independently of somePromise and the values returned by .then() and/or .catch(). In contrast:

.finally() ignores what its callback returns and simply passes on the settlement that existed before it was called:

Promise.resolve(123)
  .finally(() => {})
  .then((result) => {
    assert.equal(result, 123);
  });

Promise.reject('error')
  .finally(() => {})
  .catch((error) => {
    assert.equal(error, 'error');
  });

If, however, the .finally() callback throws an exception, the Promise returned by .finally() is rejected:

Promise.reject('error (originally)')
  .finally(() => {
    throw 'error (finally)';
  })
  .catch((error) => {
    assert.equal(error, 'error (finally)');
  });
40.1.10.1 Use case for .finally(): cleaning up

One common use case for .finally() is similar to a common use case of the synchronous finally clause: cleaning up after you are done with a resource. That should always happen, regardless of whether everything went smoothly or there was an error – for example:

let connection;
db.open()
.then((conn) => {
  connection = conn;
  return connection.select({ name: 'Jane' });
})
.then((result) => {
  // Process result
  // Use `connection` to make more queries
})
// ···
.catch((error) => {
  // handle errors
})
.finally(() => {
  connection.close();
});
40.1.10.2 Use case for .finally(): doing something first after any kind of settlement

We can also use .finally() before both .then() and .catch(). Then what we do in the .finally() callback is always executed before the other two callbacks.

For example, this is what happens with a fulfilled Promise:

Promise.resolve('fulfilled')
  .finally(() => {
    console.log('finally');
  })
  .then((result) => {
    console.log('then ' + result);
  })
  .catch((error) => {
    console.log('catch ' + error);
  })
;
// Output:
// 'finally'
// 'then fulfilled'

This is what happens with a rejected Promise:

Promise.reject('rejected')
  .finally(() => {
    console.log('finally');
  })
  .then((result) => {
    console.log('then ' + result);
  })
  .catch((error) => {
    console.log('catch ' + error);
  })
;
// Output:
// 'finally'
// 'catch rejected'

40.1.11 Advantages of promises over plain callbacks

These are some of the advantages of Promises over plain callbacks when it comes to handling one-off results:

One of the biggest advantages of Promises involves not working with them directly: they are the foundation of async functions, a synchronous-looking syntax for performing asynchronous computations. Asynchronous functions are covered in the next chapter.

40.2 Examples

Seeing Promises in action helps with understanding them. Let’s look at examples.

40.2.1 Node.js: Reading a file asynchronously

Consider the following text file person.json with JSON data in it:

{
  "first": "Jane",
  "last": "Doe"
}

Let’s look at two versions of code that reads this file and parses it into an object. First, a callback-based version. Second, a Promise-based version.

40.2.1.1 The callback-based version

The following code reads the contents of this file and converts it to a JavaScript object. It is based on Node.js-style callbacks:

import * as fs from 'fs';
fs.readFile('person.json',
  (error, text) => {
    if (error) { // (A)
      // Failure
      assert.fail(error);
    } else {
      // Success
      try { // (B)
        const obj = JSON.parse(text); // (C)
        assert.deepEqual(obj, {
          first: 'Jane',
          last: 'Doe',
        });
      } catch (e) {
        // Invalid JSON
        assert.fail(e);
      }
    }
  });

fs is a built-in Node.js module for file system operations. We use the callback-based function fs.readFile() to read a file whose name is person.json. If we succeed, the content is delivered via the parameter text as a string. In line C, we convert that string from the text-based data format JSON into a JavaScript object. JSON is an object with methods for consuming and producing JSON. It is part of JavaScript’s standard library and documented later in this book.

Note that there are two error-handling mechanisms: the if in line A takes care of asynchronous errors reported by fs.readFile(), while the try in line B takes care of synchronous errors reported by JSON.parse().

40.2.1.2 The Promise-based version

The following code uses readFileAsync(), a Promise-based version of fs.readFile() (created via util.promisify(), which is explained later):

readFileAsync('person.json')
  .then(text => { // (A)
    // Success
    const obj = JSON.parse(text);
    assert.deepEqual(obj, {
      first: 'Jane',
      last: 'Doe',
    });
  })
  .catch(err => { // (B)
    // Failure: file I/O error or JSON syntax error
    assert.fail(err);
  });

Function readFileAsync() returns a Promise. In line A, we specify a success callback via method .then() of that Promise. The remaining code in then’s callback is synchronous.

.then() returns a Promise, which enables the invocation of the Promise method .catch() in line B. We use it to specify a failure callback.

Note that .catch() lets us handle both the asynchronous errors of readFileAsync() and the synchronous errors of JSON.parse() because exceptions inside a .then() callback become rejections.

40.2.2 Browsers: Promisifying XMLHttpRequest

We have previously seen the event-based XMLHttpRequest API for downloading data in web browsers. The following function promisifies that API:

function httpGet(url) {
  return new Promise(
    (resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onload = () => {
        if (xhr.status === 200) {
          resolve(xhr.responseText); // (A)
        } else {
          // Something went wrong (404, etc.)
          reject(new Error(xhr.statusText)); // (B)
        }
      }
      xhr.onerror = () => {
        reject(new Error('Network error')); // (C)
      };
      xhr.open('GET', url);
      xhr.send();
    });
}

Note how the results and errors of XMLHttpRequest are handled via resolve() and reject():

This is how to use httpGet():

httpGet('http://example.com/textfile.txt')
  .then(content => {
    assert.equal(content, 'Content of textfile.txt\n');
  })
  .catch(error => {
    assert.fail(error);
  });

  Exercise: Timing out a Promise

exercises/promises/promise_timeout_test.mjs

40.2.3 Node.js: util.promisify()

util.promisify() is a utility function that converts a callback-based function f into a Promise-based one. That is, we are going from this type signature:

f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void

To this type signature:

f(arg_1, ···, arg_n) : Promise<T>

The following code promisifies the callback-based fs.readFile() (line A) and uses it:

import * as fs from 'fs';
import {promisify} from 'util';

const readFileAsync = promisify(fs.readFile); // (A)

readFileAsync('some-file.txt', {encoding: 'utf8'})
  .then(text => {
    assert.equal(text, 'The content of some-file.txt\n');
  })
  .catch(err => {
    assert.fail(err);
  });

  Exercises: util.promisify()

40.2.4 Browsers: Fetch API

All modern browsers support Fetch, a new Promise-based API for downloading data. Think of it as a Promise-based version of XMLHttpRequest. The following is an excerpt of the API:

interface Body {
  text() : Promise<string>;
  ···
}
interface Response extends Body {
  ···
}
declare function fetch(str) : Promise<Response>;

That means we can use fetch() as follows:

fetch('http://example.com/textfile.txt')
  .then(response => response.text())
  .then(text => {
    assert.equal(text, 'Content of textfile.txt\n');
  });

  Exercise: Using the fetch API

exercises/promises/fetch_json_test.mjs

40.3 Error handling: don’t mix rejections and exceptions

Rule for implementing functions and methods:

Don’t mix (asynchronous) rejections and (synchronous) exceptions.

This makes our synchronous and asynchronous code more predictable and simpler because we can always focus on a single error-handling mechanism.

For Promise-based functions and methods, the rule means that they should never throw exceptions. Alas, it is easy to accidentally get this wrong – for example:

// Don’t do this
function asyncFunc() {
  doSomethingSync(); // (A)
  return doSomethingAsync()
    .then(result => {
      // ···
    });
}

The problem is that if an exception is thrown in line A, then asyncFunc() will throw an exception. Callers of that function only expect rejections and are not prepared for an exception. There are three ways in which we can fix this issue.

We can wrap the whole body of the function in a try-catch statement and return a rejected Promise if an exception is thrown:

// Solution 1
function asyncFunc() {
  try {
    doSomethingSync();
    return doSomethingAsync()
      .then(result => {
        // ···
      });
  } catch (err) {
    return Promise.reject(err);
  }
}

Given that .then() converts exceptions to rejections, we can execute doSomethingSync() inside a .then() callback. To do so, we start a Promise chain via Promise.resolve(). We ignore the fulfillment value undefined of that initial Promise.

// Solution 2
function asyncFunc() {
  return Promise.resolve()
    .then(() => {
      doSomethingSync();
      return doSomethingAsync();
    })
    .then(result => {
      // ···
    });
}

Lastly, new Promise() also converts exceptions to rejections. Using this constructor is therefore similar to the previous solution:

// Solution 3
function asyncFunc() {
  return new Promise((resolve, reject) => {
      doSomethingSync();
      resolve(doSomethingAsync());
    })
    .then(result => {
      // ···
    });
}

40.4 Promise-based functions start synchronously, settle asynchronously

Most Promise-based functions are executed as follows:

The following code demonstrates that:

function asyncFunc() {
  console.log('asyncFunc');
  return new Promise(
    (resolve, _reject) => {
      console.log('new Promise()');
      resolve();
    });
}
console.log('START');
asyncFunc()
  .then(() => {
    console.log('.then()'); // (A)
  });
console.log('END');

// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'

We can see that the callback of new Promise() is executed before the end of the code, while the result is delivered later (line A).

Benefits of this approach:

  More information on this approach

“Designing APIs for Asynchrony” by Isaac Z. Schlueter

40.5 Promise combinator functions: working with Arrays of Promises

40.5.1 What is a Promise combinator function?

The combinator pattern is a pattern in functional programming for building structures. It is based on two kinds of functions:

When it comes to JavaScript Promises:

Next, we’ll take a closer look at the mentioned Promise combinators.

40.5.2 Promise.all()

This is the type signature of Promise.all():

Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>

Promise.all() returns a Promise which is:

This is a quick demo of the output Promise being fulfilled:

const promises = [
  Promise.resolve('result a'),
  Promise.resolve('result b'),
  Promise.resolve('result c'),
];
Promise.all(promises)
  .then((arr) => assert.deepEqual(
    arr, ['result a', 'result b', 'result c']
  ));

The following example demonstrates what happens if at least one of the input Promises is rejected:

const promises = [
  Promise.resolve('result a'),
  Promise.resolve('result b'),
  Promise.reject('ERROR'),
];
Promise.all(promises)
  .catch((err) => assert.equal(
    err, 'ERROR'
  ));

Fig. 23 illustrates how Promise.all() works.

Figure 23: The Promise combinator Promise.all().
40.5.2.1 Asynchronous .map() via Promise.all()

Array transformation methods such as .map(), .filter(), etc., are made for synchronous computations. For example:

function timesTwoSync(x) {
  return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);

What happens if the callback of .map() is a Promise-based function (a function that maps normal values to Promises)? Then the result of .map() is an Array of Promises. Alas, that is not data that normal code can work with. Thankfully, we can fix that via Promise.all(): It converts an Array of Promises into a Promise that is fulfilled with an Array of normal values.

function timesTwoAsync(x) {
  return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
  .then(result => {
    assert.deepEqual(result, [2, 4, 6]);
  });
40.5.2.2 A more realistic .map() example

Next, we’ll use .map() and Promise.all() to downlooad text files from the web. For that, we need the following tool function:

function downloadText(url) {
  return fetch(url)
    .then((response) => { // (A)
      if (!response.ok) { // (B)
        throw new Error(response.statusText);
      }
      return response.text(); // (C)
    });
}

downloadText() uses the Promise-based fetch API to download a text file as a string:

In the following example, we download two text files:

const urls = [
  'http://example.com/first.txt',
  'http://example.com/second.txt',
];

const promises = urls.map(
  url => downloadText(url));

Promise.all(promises)
  .then(
    (arr) => assert.deepEqual(
      arr, ['First!', 'Second!']
    ));
40.5.2.3 A simple implementation of Promise.all()

This is a simplified implementation of Promise.all() (e.g., it performs no safety checks):

function all(iterable) {
  return new Promise((resolve, reject) => {
    let elementCount = 0;
    let result;

    let index = 0;
    for (const promise of iterable) {
      // Preserve the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => {
          result[currentIndex] = value;
          elementCount++;
          if (elementCount === result.length) {
            resolve(result); // (A)
          }
        },
        (err) => {
          reject(err); // (B)
        });
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    // Now we know how many Promises there are in `iterable`.
    // We can wait until now with initializing `result` because
    // the callbacks of .then() are executed asynchronously.
    result = new Array(index);
  });
}

The two main locations where the result Promise is settled are line A and line B. After one of them settled, the other can’t change the settlement value anymore because a Promise can only be settled once.

40.5.3 Promise.race()

This is the type signature of Promise.race():

Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>

Promise.race() returns a Promise q which is settled as soon as the first Promise p among promises is settled. q has the same settlement value as p.

In the following demo, the settlement of the fulfilled Promise (line A) happens before the settlement of the rejected Promise (line B). Therefore, the result is also fulfilled (line C).

const promises = [
  new Promise((resolve, reject) =>
    setTimeout(() => resolve('result'), 100)), // (A)
  new Promise((resolve, reject) =>
    setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
  .then((result) => assert.equal( // (C)
    result, 'result'));

In the next demo, the rejection happens first:

const promises = [
  new Promise((resolve, reject) =>
    setTimeout(() => resolve('result'), 200)),
  new Promise((resolve, reject) =>
    setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
  .then(
    (result) => assert.fail(),
    (err) => assert.equal(
      err, 'ERROR'));

Note that the Promise returned by Promise.race() is settled as soon as the first among its input Promises is settled. That means that the result of Promise.race([]) is never settled.

Fig. 24 illustrates how Promise.race() works.

Figure 24: The Promise combinator Promise.race().
40.5.3.1 Using Promise.race() to time out a Promise

In this section, we are going to use Promise.race() to time out Promises. The following helper function will be useful several times:

function resolveAfter(ms, value=undefined) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), ms);
  });
}

resolveAfter() returns a Promise that is resolved with value after ms milliseconds.

This function times out a Promise:

function timeout(timeoutInMs, promise) {
  return Promise.race([
    promise,
    resolveAfter(timeoutInMs,
      Promise.reject(new Error('Operation timed out'))),
  ]);
}

timeout() returns a Promise whose settlement is the same as the one of whichever Promise settles first among the following two:

  1. The parameter promise
  2. A Promise that is rejected after timeoutInMs milliseconds

To produce the second Promise, timeout() uses the fact that resolving a pending Promise with a rejected Promise leads to the former being rejected.

Let’s see timeout() in action. Here, the input Promise is fulfilled before the timeout. Therefore, the output Promise is fulfilled.

timeout(200, resolveAfter(100, 'Result!'))
  .then(result => assert.equal(result, 'Result!'));

Here, the timeout happens before the input Promise is fulfilled. Therefore, the output Promise is rejected.

timeout(100, resolveAfter(2000, 'Result!'))
  .catch(err => assert.deepEqual(err, new Error('Operation timed out')));

It is important to understand what “timing out a Promise” really means:

That is, timing out only prevents the input Promise from affecting the output (since a Promise can only be settled once). But it does not stop the asynchronous operation that produced the input Promise.

40.5.3.2 A simple implementation of Promise.race()

This is a simplified implementation of Promise.race() (e.g., it performs no safety checks):

function race(iterable) {
  return new Promise((resolve, reject) => {
    for (const promise of iterable) {
      promise.then(
        (value) => {
          resolve(value); // (A)
        },
        (err) => {
          reject(err); // (B)
        });
    }
  });
}

The result Promise is settled in either line A or line B. Once it is, the settlement value can’t be changed anymore.

40.5.4 Promise.any() and AggregateError [ES2021]

This is the type signature of Promise.any():

Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>

Promise.any() returns a Promise p. How it is settled, depends on the parameter promises (which refers to an iterable over Promises):

This is the type signature of AggregateError (a subclass of Error):

class AggregateError extends Error {
  // Instance properties (complementing the ones of Error)
  errors: Array<any>;

  constructor(
    errors: Iterable<any>,
    message: string = '',
    options?: ErrorOptions // ES2022
  );
}
interface ErrorOptions {
  cause?: any; // ES2022
}

Fig. 25 illustrates how Promise.any() works.

Figure 25: The Promise combinator Promise.any().
40.5.4.1 Two first examples

This is what happens if one Promise is fulfilled:

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.resolve('result'),
];
Promise.any(promises)
  .then((result) => assert.equal(
    result, 'result'
  ));

This is what happens if all Promises are rejected:

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.reject('ERROR C'),
];
Promise.any(promises)
  .catch((aggregateError) => assert.deepEqual(
    aggregateError.errors,
    ['ERROR A', 'ERROR B', 'ERROR C']
  ));
40.5.4.2 Promise.any() vs. Promise.all()

There are two ways in which Promise.any() and Promise.all() can be compared:

40.5.4.3 Promise.any() vs. Promise.race()

Promise.any() and Promise.race() are also related, but interested in different things:

The main – relatively rare – use case for .race() is timing out Promises. The use cases for .any() are broader. We’ll look at them next.

40.5.4.4 Use cases for Promise.any()

We use Promise.any() if we have multiple asynchronous computations and we are only interested in the first successful one. In a way, we let the computations compete with each other and use whichever one is fastest.

The following code demonstrates what that looks like when downloading resources:

const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

The same pattern enables us to use whichever module downloads more quickly:

const lodash = await Promise.any([
  import('https://primary.example.com/lodash'),
  import('https://secondary.example.com/lodash'),
]);

For comparison, this is the code we’d use if the secondary server is only a fallback – in case the primary server fails:

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
40.5.4.5 How would we implement Promise.any()?

A simple implementation of Promise.any() is basically a mirror version of the implementation of Promise.all().

40.5.5 Promise.allSettled() [ES2020]

This time, the type signatures are a little more complicated. Feel free to skip ahead to the first demo, which should be easier to understand.

This is the type signature of Promise.allSettled():

Promise.allSettled<T>(promises: Iterable<Promise<T>>)
  : Promise<Array<SettlementObject<T>>>

It returns a Promise for an Array whose elements have the following type signature:

type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;

interface FulfillmentObject<T> {
  status: 'fulfilled';
  value: T;
}

interface RejectionObject {
  status: 'rejected';
  reason: unknown;
}

Promise.allSettled() returns a Promise out. Once all promises are settled, out is fulfilled with an Array. Each element e of that Array corresponds to one Promise p of promises:

Unless there is an error when iterating over promises, the output Promise out is never rejected.

Fig. 26 illustrates how Promise.allSettled() works.

Figure 26: The Promise combinator Promise.allSettled().
40.5.5.1 A first demo of Promise.allSettled()

This is a quick first demo of how Promise.allSettled() works:

Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
  { status: 'fulfilled', value:  'a' },
  { status: 'rejected',  reason: 'b' },
]));
40.5.5.2 A longer example for Promise.allSettled()

The next example is similar to the .map() plus Promise.all() example (from which we are borrowing the function downloadText()): We are downloading multiple text files whose URLs are stored in an Array. However, this time, we don’t want to stop when there is an error, we want to keep going. Promise.allSettled() allows us to do that:

const urls = [
  'http://example.com/exists.txt',
  'http://example.com/missing.txt',
];

const result = Promise.allSettled(
  urls.map(u => downloadText(u)));
result.then(
  arr => assert.deepEqual(
    arr,
    [
      {
        status: 'fulfilled',
        value: 'Hello!',
      },
      {
        status: 'rejected',
        reason: new Error('Not Found'),
      },
    ]
));
40.5.5.3 A simple implementation of Promise.allSettled()

This is a simplified implementation of Promise.allSettled() (e.g., it performs no safety checks):

function allSettled(iterable) {
  return new Promise((resolve, reject) => {
    let elementCount = 0;
    let result;

    function addElementToResult(i, elem) {
      result[i] = elem;
      elementCount++;
      if (elementCount === result.length) {
        resolve(result);
      }
    }

    let index = 0;
    for (const promise of iterable) {
      // Capture the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => addElementToResult(
          currentIndex, {
            status: 'fulfilled',
            value
          }),
        (reason) => addElementToResult(
          currentIndex, {
            status: 'rejected',
            reason
          }));
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    // Now we know how many Promises there are in `iterable`.
    // We can wait until now with initializing `result` because
    // the callbacks of .then() are executed asynchronously.
    result = new Array(index);
  });
}

40.5.6 Short-circuiting (advanced)

For a Promise combinator, short-circuiting means that the output Promise is settled early – before all input Promises are settled. The following combinators short-circuit:

Once again, settling early does not mean that the operations behind the ignored Promises are stopped. It just means that their settlements are ignored.

40.6 Concurrency and Promise.all() (advanced)

40.6.1 Sequential execution vs. concurrent execution

Consider the following code:

const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');

asyncFunc1()
  .then(result1 => {
    assert.equal(result1, 'one');
    return asyncFunc2();
  })
  .then(result2 => {
    assert.equal(result2, 'two');
  });

Using .then() in this manner executes Promise-based functions sequentially: only after the result of asyncFunc1() is settled will asyncFunc2() be executed.

Promise.all() helps execute Promise-based functions more concurrently:

Promise.all([asyncFunc1(), asyncFunc2()])
  .then(arr => {
    assert.deepEqual(arr, ['one', 'two']);
  });

40.6.2 Concurrency tip: focus on when operations start

Tip for determining how “concurrent” asynchronous code is: Focus on when asynchronous operations start, not on how their Promises are handled.

For example, each of the following functions executes asyncFunc1() and asyncFunc2() concurrently because they are started at nearly the same time.

function concurrentAll() {
  return Promise.all([asyncFunc1(), asyncFunc2()]);
}

function concurrentThen() {
  const p1 = asyncFunc1();
  const p2 = asyncFunc2();
  return p1.then(r1 => p2.then(r2 => [r1, r2]));
}

On the other hand, both of the following functions execute asyncFunc1() and asyncFunc2() sequentially: asyncFunc2() is only invoked after the Promise of asyncFunc1() is fulfilled.

function sequentialThen() {
  return asyncFunc1()
    .then(r1 => asyncFunc2()
      .then(r2 => [r1, r2]));
}

function sequentialAll() {
  const p1 = asyncFunc1();
  const p2 = p1.then(() => asyncFunc2());
  return Promise.all([p1, p2]);
}

40.6.3 Promise.all() is fork-join

Promise.all() is loosely related to the concurrency pattern “fork join”. Let’s revisit an example that we have encountered previously:

Promise.all([
    // (A) fork
    downloadText('http://example.com/first.txt'),
    downloadText('http://example.com/second.txt'),
  ])
  // (B) join
  .then(
    (arr) => assert.deepEqual(
      arr, ['First!', 'Second!']
    ));

40.7 Tips for chaining Promises

This section gives tips for chaining Promises.

40.7.1 Chaining mistake: losing the tail

Problem:

// Don’t do this
function foo() {
  const promise = asyncFunc();
  promise.then(result => {
    // ···
  });

  return promise;
}

Computation starts with the Promise returned by asyncFunc(). But afterward, computation continues and another Promise is created via .then(). foo() returns the former Promise, but should return the latter. This is how to fix it:

function foo() {
  const promise = asyncFunc();
  return promise.then(result => {
    // ···
  });
}

40.7.2 Chaining mistake: nesting

Problem:

// Don’t do this
asyncFunc1()
  .then(result1 => {
    return asyncFunc2()
    .then(result2 => { // (A)
      // ···
    });
  });

The .then() in line A is nested. A flat structure would be better:

asyncFunc1()
  .then(result1 => {
    return asyncFunc2();
  })
  .then(result2 => {
    // ···
  });

40.7.3 Chaining mistake: more nesting than necessary

This is another example of avoidable nesting:

// Don’t do this
asyncFunc1()
  .then(result1 => {
    if (result1 < 0) {
      return asyncFuncA()
      .then(resultA => 'Result: ' + resultA);
    } else {
      return asyncFuncB()
      .then(resultB => 'Result: ' + resultB);
    }
  });

We can once again get a flat structure:

asyncFunc1()
  .then(result1 => {
    return result1 < 0 ? asyncFuncA() : asyncFuncB();
  })
  .then(resultAB => {
    return 'Result: ' + resultAB;
  });

40.7.4 Not all nesting is bad

In the following code, we actually benefit from nesting:

db.open()
  .then(connection => { // (A)
    return connection.select({ name: 'Jane' })
      .then(result => { // (B)
        // Process result
        // Use `connection` to make more queries
      })
      // ···
      .finally(() => {
        connection.close(); // (C)
      });
  })

We are receiving an asynchronous result in line A. In line B, we are nesting so that we have access to variable connection inside the callback and in line C.

40.7.5 Chaining mistake: creating Promises instead of chaining

Problem:

// Don’t do this
class Model {
  insertInto(db) {
    return new Promise((resolve, reject) => { // (A)
      db.insert(this.fields)
        .then(resultCode => {
          this.notifyObservers({event: 'created', model: this});
          resolve(resultCode);
        }).catch(err => {
          reject(err);
        })
    });
  }
  // ···
}

In line A, we are creating a Promise to deliver the result of db.insert(). That is unnecessarily verbose and can be simplified:

class Model {
  insertInto(db) {
    return db.insert(this.fields)
      .then(resultCode => {
        this.notifyObservers({event: 'created', model: this});
        return resultCode;
      });
  }
  // ···
}

The key idea is that we don’t need to create a Promise; we can return the result of the .then() call. An additional benefit is that we don’t need to catch and re-reject the failure of db.insert(). We simply pass its rejection on to the caller of .insertInto().

40.8 Quick reference: Promise combinator functions

Unless noted otherwise, the functionality was introduced in ECMAScript 6 (which is when Promises were added to the language).

Glossary:

40.8.1 Promise.all()

Promise.all<T>(promises: Iterable<Promise<T>>)
  : Promise<Array<T>>

40.8.2 Promise.race()

Promise.race<T>(promises: Iterable<Promise<T>>)
  : Promise<T>

40.8.3 Promise.any() [ES2021]

Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>

This is the type signature of AggregateError (a few members were omitted):

class AggregateError {
  constructor(errors: Iterable<any>, message: string);
  get errors(): Array<any>;
  get message(): string;
}

40.8.4 Promise.allSettled() [ES2020]

Promise.allSettled<T>(promises: Iterable<Promise<T>>)
  : Promise<Array<SettlementObject<T>>>

This is the type signature of SettlementObject:

type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;

interface FulfillmentObject<T> {
  status: 'fulfilled';
  value: T;
}

interface RejectionObject {
  status: 'rejected';
  reason: unknown;
}