JavaScript for impatient programmers (ES2022 edition)
この本をサポートしてください: 購入 または 寄付
(広告です。ブロックしないでください。)

40 非同期プログラミングのためのPromise [ES6]



  推奨される読書

この章は、JavaScriptにおける非同期プログラミングの背景について、前の章に基づいています。

40.1 Promiseの基本的な使い方

Promiseは、非同期的に結果を配信するための手法です。

40.1.1 Promiseベースの関数の使用

以下のコードは、Promiseベースの関数 addAsync()(その実装は後で示します)を使用する例です。

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

Promiseはイベントパターンに似ています。コールバックを登録するオブジェクト(Promise)があります。

Promiseベースの関数はPromiseを返し、結果またはエラー(完了した場合)を送信します。Promiseはそれを関連するコールバックに渡します。

イベントパターンとは対照的に、Promiseは1回限りの結果に最適化されています。

40.1.2 Promiseとは何か?

Promiseとは何でしょうか?それを見るには2つの方法があります。

40.1.3 Promiseベースの関数の実装

これは、2つの数値 xy を加算するPromiseベースの関数の実装です。

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() は、すぐに Promise コンストラクタを呼び出します。その関数の実際の実装は、そのコンストラクタ(A行)に渡されるコールバック内にあります。そのコールバックには、2つの関数が提供されます。

40.1.4 Promiseの状態

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.

22 は、Promiseがなりうる3つの状態を示しています。Promiseは1回限りの結果に特化しており、競合状態(早すぎるまたは遅すぎる登録)から私たちを保護します。

さらに、Promiseが確定すると、その状態と確定値はもう変更できません。これにより、コードの予測可能性が高まり、Promiseの1回限りの性質が強制されます。

  確定しないPromiseもある

Promiseがまったく確定しない可能性があります。例えば

new Promise(() => {})

40.1.5 Promise.resolve(): 指定された値で履行されたPromiseを作成

Promise.resolve(x) は、値 x で履行されるPromiseを作成します。

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

パラメータがすでにPromiseの場合、変更されずに返されます。

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

したがって、任意の値 x が与えられた場合、Promise.resolve(x) を使用して、Promiseがあることを保証できます。

名前が fulfill ではなく resolve であることに注意してください。これは、.resolve() が、そのパラメータが拒否されたPromiseである場合、拒否されたPromiseを返すためです。

40.1.6 Promise.reject(): 指定された値で拒否されたPromiseを作成

Promise.reject(err) は、値 err で拒否されるPromiseを作成します。

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

40.1.7 .then() コールバックでの返り値と例外のスロー

.then() はPromiseの履行を処理します。また、新しいPromiseを返します。そのPromiseがどのように確定するかは、コールバック内で何が起こるかによって異なります。3つの一般的なケースを見てみましょう。

40.1.7.1 Promiseでない値を返す

まず、コールバックはPromiseでない値を返すことができます(A行)。その結果、.then() によって返されるPromiseは、その値で履行されます(B行で確認したように)。

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

次に、コールバックはPromise p を返すことができます(A行)。その結果、p.then() が返すもの「になります」。言い換えれば、.then() がすでに返したPromiseは、実質的に p に置き換えられます。

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

これはなぜ便利なのでしょうか?Promiseベースの操作の結果を返し、その履行値を「フラットな」(ネストされていない).then() で処理できます。比較してください

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

// Nested
asyncFunc1()
  .then(result1 => {
    /*···*/
    asyncFunc2()
    .then(result2 => {
      /*···*/
    });
  });
40.1.7.3 例外のスロー

3番目に、コールバックは例外をスローできます。その結果、.then() によって返されるPromiseは、その例外で拒否されます。つまり、同期エラーは非同期エラーに変換されます。

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

40.1.8 .catch() とそのコールバック

.then().catch() の違いは、後者が履行ではなく拒否によってトリガーされることです。ただし、どちらのメソッドも、コールバックのアクションを同じ方法でPromiseに変えます。例えば、次のコードでは、A行の .catch() コールバックによって返される値が履行値になります。

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 メソッド呼び出しのチェーン

.then().catch() は常にPromiseを返します。これにより、任意に長いメソッド呼び出しのチェーンを作成できます。

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
    });
}

チェーンにより、A行の return は最後の .then() の結果を返します。

ある意味で、.then() は同期セミコロンの非同期バージョンです。

また、.catch() をミックスに追加して、複数のエラーソースを同時に処理させることができます。

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]

Promiseメソッド .finally() は、多くの場合、次のように使用されます。

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

.finally() コールバックは、somePromise および .then().catch() によって返される値に関係なく、常に実行されます。対照的に

.finally() は、そのコールバックが返すものを無視し、呼び出される前に存在していた確定をそのまま渡します。

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

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

ただし、.finally() コールバックが例外をスローすると、.finally() によって返されるPromiseは拒否されます。

Promise.reject('error (originally)')
  .finally(() => {
    throw 'error (finally)';
  })
  .catch((error) => {
    assert.equal(error, 'error (finally)');
  });
40.1.10.1 .finally() のユースケース: クリーンアップ

.finally() の一般的なユースケースの1つは、同期 finally 句の一般的なユースケースと同様です。リソースの完了後にクリーンアップすることです。これは、すべてがスムーズに進んだか、エラーが発生したかに関係なく、常に発生する必要があります。例えば

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 .finally() のユースケース: あらゆる種類の確定後、最初に何かを行う

.then().catch() の両方の前に .finally() を使用することもできます。.finally() コールバックで行うことは、常に他の 2 つのコールバックよりも先に実行されます。

たとえば、これは履行された Promise で起こることです

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

これは、拒否された 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 プレーンなコールバックに対する Promise の利点

以下は、1 回限りの結果を処理する場合に、プレーンなコールバックに対する Promise の利点の一部です。

Promise の最大の利点の 1 つは、直接操作する必要がないことです。Promise は、非同期計算を実行するための同期のように見える構文である非同期関数の基礎です。非同期関数については、次の章で説明します。

40.2 例

Promise の動作を見ることは、Promise を理解するのに役立ちます。例を見てみましょう。

40.2.1 Node.js: ファイルの非同期読み取り

JSON データが含まれている次のテキストファイル person.json を検討してください。

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

このファイルを読み取り、オブジェクトに解析するコードの 2 つのバージョンを見てみましょう。最初に、コールバックベースのバージョン。次に、Promise ベースのバージョンです。

40.2.1.1 コールバックベースのバージョン

次のコードは、このファイルの内容を読み取り、JavaScript オブジェクトに変換します。これは、Node.js スタイルのコールバックに基づいています

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 は、ファイルシステム操作のための組み込みの Node.js モジュールです。コールバックベースの関数 fs.readFile() を使用して、名前が person.json のファイルを読み取ります。成功すると、コンテンツはパラメーター text を介して文字列として配信されます。C 行で、その文字列をテキストベースのデータ形式 JSON から JavaScript オブジェクトに変換します。JSON は、JSON を消費および生成するためのメソッドを持つオブジェクトです。これは JavaScript の標準ライブラリの一部であり、この本の後半で説明されています。

エラー処理メカニズムが 2 つあることに注意してください。A 行の iffs.readFile() によって報告された非同期エラーを処理し、B 行の tryJSON.parse() によって報告された同期エラーを処理します。

40.2.1.2 Promise ベースのバージョン

次のコードでは、fs.readFile() の Promise ベースのバージョンである readFileAsync() を使用します(後で説明する util.promisify() を介して作成されます)

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);
  });

関数 readFileAsync() は Promise を返します。A 行で、その Promise のメソッド .then() を介して成功コールバックを指定します。then のコールバック内の残りのコードは同期です。

.then() は Promise を返し、これにより B 行で Promise メソッド .catch() を呼び出すことができます。.catch() を使用して、失敗コールバックを指定します。

.catch() を使用すると、readFileAsync() の非同期エラーと JSON.parse() の同期エラーの両方を処理できることに注意してください。これは、.then() コールバック内の例外が拒否になるためです。

40.2.2 ブラウザー: XMLHttpRequest の Promise 化

Web ブラウザーでデータをダウンロードするためのイベントベースの XMLHttpRequest API については、以前に説明しました。次の関数は、その API を Promise 化します

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();
    });
}

XMLHttpRequest の結果とエラーが resolve() および reject() を介してどのように処理されるかに注意してください。

これは httpGet() を使用する方法です

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

  演習: Promise のタイムアウト

exercises/promises/promise_timeout_test.mjs

40.2.3 Node.js: util.promisify()

util.promisify() は、コールバックベースの関数 f を Promise ベースの関数に変換するユーティリティ関数です。つまり、次の型シグネチャから

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

次の型シグネチャに移行します

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

次のコードは、コールバックベースの fs.readFile() を Promise 化し(A 行)、それを使用します

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);
  });

  演習: util.promisify()

40.2.4 ブラウザー: Fetch API

すべての最新のブラウザーは、データをダウンロードするための新しい Promise ベースの API である Fetch をサポートしています。XMLHttpRequest の Promise ベースのバージョンと考えてください。以下は、API の抜粋です

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

つまり、fetch() は次のように使用できます

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

  演習: fetch API の使用

exercises/promises/fetch_json_test.mjs

40.3 エラー処理: 拒否と例外を混同しない

関数とメソッドを実装するためのルール

(非同期)拒否と(同期)例外を混同しないでください。

これにより、常に単一のエラー処理メカニズムに集中できるため、同期コードと非同期コードの予測可能性が高まり、シンプルになります。

Promise ベースの関数とメソッドの場合、このルールは、例外をスローしてはならないことを意味します。残念ながら、これを間違えるのは簡単です。たとえば、

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

問題は、A 行で例外がスローされた場合、asyncFunc() が例外をスローすることです。その関数の呼び出し元は、拒否のみを期待しており、例外に対応していません。この問題を解決するには、3 つの方法があります。

関数の本体全体を try-catch ステートメントでラップし、例外がスローされた場合は拒否された Promise を返すことができます

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

.then() が例外を拒否に変換することを考慮すると、.then() コールバック内で doSomethingSync() を実行できます。これを行うには、Promise.resolve() を介して Promise チェーンを開始します。その最初の Promise の履行値 undefined は無視します。

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

最後に、new Promise() も例外を拒否に変換します。したがって、このコンストラクターを使用することは、前の解決策に似ています

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

40.4 Promise ベースの関数は同期的に開始し、非同期的に完了します

ほとんどの Promise ベースの関数は、次のように実行されます

次のコードはそれを示しています

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()'

new Promise() のコールバックはコードの終了前に実行されますが、結果は後で配信されることがわかります(A 行)。

このアプローチの利点

  このアプローチに関する詳細情報

Isaac Z. Schlueter による「非同期処理のための API の設計」

40.5 Promise コンビネーター関数: Promise の配列の操作

40.5.1 Promise コンビネーター関数とは何ですか?

コンビネーターパターンは、構造を構築するための関数型プログラミングのパターンです。これは、2 種類の関数に基づいています

JavaScript Promise に関しては

次に、言及した Promise コンビネーターについて詳しく見ていきます。

40.5.2 Promise.all()

これは Promise.all() の型シグネチャです

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

Promise.all() は、次のような Promise を返します。

これは、出力 Promise が履行されることの簡単なデモです

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']
  ));

次の例は、入力 Promise の少なくとも 1 つが拒否された場合に何が起こるかを示しています

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

23 は、Promise.all() がどのように機能するかを示しています。

Figure 23: The Promise combinator Promise.all().
40.5.2.1 Promise.all() による非同期 .map()

.map().filter() などの配列変換メソッドは、同期計算用です。たとえば

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

.map() のコールバックが Promise ベースの関数(通常の値を Promise にマップする関数)である場合はどうなるでしょうか? その場合、.map() の結果は Promise の配列になります。残念ながら、それは通常のコードが操作できるデータではありません。ありがたいことに、Promise.all() を使用して修正できます。Promise の配列を、通常の値の配列で履行される Promise に変換します。

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 より現実的な .map() の例

次に、.map()Promise.all() を使用して、Web からテキストファイルをダウンロードします。そのためには、次のツール関数が必要です

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

downloadText() は、Promise ベースの fetch API を使用して、テキストファイルを文字列としてダウンロードします

次の例では、2 つのテキストファイルをダウンロードします

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 Promise.all() の簡単な実装

これは、Promise.all() の簡略化された実装です(たとえば、安全性のチェックは実行しません)

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);
  });
}

結果 Promise が完了する主な場所は、A 行と B 行の 2 つです。それらのいずれかが完了した後、Promise は 1 回しか完了できないため、もう一方の結果を変更することはできません。

40.5.3 Promise.race()

これは Promise.race() の型シグネチャです

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

Promise.race() は、promises の中の最初の Promise p が完了するとすぐに完了する Promise q を返します。qp と同じ完了値を持っています。

次のデモでは、履行された Promise の完了(A 行)は、拒否された Promise の完了(B 行)よりも先に発生します。したがって、結果も履行されます(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'));

次のデモでは、拒否が最初に発生します

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'));

Promise.race() によって返される Promise は、その入力 Promise の最初のものが完了するとすぐに完了することに注意してください。つまり、Promise.race([]) の結果は決して完了しません。

24 は、Promise.race() がどのように機能するかを示しています。

Figure 24: The Promise combinator Promise.race().
40.5.3.1 Promise.race() を使用した Promise のタイムアウト

このセクションでは、Promise.race() を使用して Promise をタイムアウトにします。次のヘルパー関数は、複数回役立ちます

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

resolveAfter() は、ms ミリ秒後に value で解決される Promise を返します。

この関数はPromiseをタイムアウトさせます。

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

timeout() は、以下の2つのPromiseのうち、最初に確定した方のPromiseと同じ確定状態を持つPromiseを返します。

  1. パラメータ promise
  2. timeoutInMsミリ秒後にリジェクトされるPromise

2番目のPromiseを生成するために、timeout()は、保留中のPromiseをリジェクトされたPromiseで解決すると、前者がリジェクトされるという事実を利用します。

timeout() の動作を見てみましょう。ここでは、入力Promiseがタイムアウト前にフルフィルされます。したがって、出力Promiseもフルフィルされます。

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

ここでは、入力Promiseがフルフィルされる前にタイムアウトが発生します。したがって、出力Promiseはリジェクトされます。

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

「Promiseをタイムアウトさせる」ことが実際に何を意味するのかを理解することが重要です。

つまり、タイムアウトは入力Promiseが出力に影響を与えるのを防ぐだけです(Promiseは一度しか確定できないため)。しかし、入力Promiseを生成した非同期操作を停止させるわけではありません。

40.5.3.2 Promise.race() の簡単な実装

これは Promise.race() の簡略化された実装です(例:安全性のチェックは行いません)。

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

結果のPromiseは、A行またはB行のいずれかで確定します。一度確定すると、その確定値を変更することはできません。

40.5.4 Promise.any()AggregateError [ES2021]

これは Promise.any() の型シグネチャです。

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

Promise.any() はPromise p を返します。どのように確定するかは、パラメータ promises (Promiseのイテラブルを参照)によって異なります。

これは AggregateError の型シグネチャです(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
}

25は、Promise.any() の動作を示しています。

Figure 25: The Promise combinator Promise.any().
40.5.4.1 最初の2つの例

これは、1つのPromiseがフルフィルされた場合に起こることです。

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

これは、すべてのPromiseがリジェクトされた場合に起こることです。

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()

Promise.any()Promise.all() を比較するには2つの方法があります。

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

Promise.any()Promise.race() も関連していますが、関心のあるものが異なります。

.race() の主な、比較的まれなユースケースは、Promiseをタイムアウトさせることです。.any() のユースケースはより広範です。次にそれらを見ていきます。

40.5.4.4 Promise.any() のユースケース

複数の非同期計算があり、最初の成功したもののみに関心がある場合は、Promise.any() を使用します。ある意味で、計算同士を競わせ、最も速いものを利用します。

次のコードは、リソースをダウンロードする際にどのように見えるかを示しています。

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()),
]);

同じパターンで、より高速にダウンロードできるモジュールを使用できます。

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

比較のために、セカンダリサーバーがプライマリサーバーが失敗した場合のフォールバックにすぎない場合に使用するコードを次に示します。

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
40.5.4.5 どのように Promise.any() を実装するのか?

Promise.any() の簡単な実装は、基本的に Promise.all() の実装のミラーバージョンです。

40.5.5 Promise.allSettled() [ES2020]

今回は、型シグネチャが少し複雑になっています。理解しやすいはずの最初のデモに進んでください。

これは Promise.allSettled() の型シグネチャです。

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

これは、要素が次の型シグネチャを持つ配列のPromiseを返します。

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

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

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

Promise.allSettled() はPromise out を返します。すべての promises が確定すると、out は配列でフルフィルされます。その配列の各要素 e は、promises の1つのPromise p に対応します。

promises の反復処理中にエラーが発生しない限り、出力Promise out がリジェクトされることはありません。

26は、Promise.allSettled() の動作を示しています。

Figure 26: The Promise combinator Promise.allSettled().
40.5.5.1 Promise.allSettled() の最初のデモ

これは、Promise.allSettled() の動作を示す最初の簡単なデモです。

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 Promise.allSettled() のより長い例

次の例は、.map() プラス Promise.all() の例(関数 downloadText() を借用)と似ています。URLが配列に格納されている複数のテキストファイルをダウンロードしています。ただし、今回はエラーが発生した場合に停止したくありません。継続したいのです。Promise.allSettled() を使用すると、それが可能になります。

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 Promise.allSettled() の簡単な実装

これは、Promise.allSettled() の簡略化された実装です(例:安全性のチェックは行いません)。

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 ショートサーキット(高度)

Promiseコンビネータの場合、ショートサーキットとは、出力Promiseがすべての入力Promiseが確定する前に早期に確定することを意味します。次のコンビネータはショートサーキットします。

繰り返しますが、早期に確定しても、無視されたPromiseの背後にある操作が停止されるわけではありません。単にそれらの確定が無視されるだけです。

40.6 並行処理と Promise.all() (高度)

40.6.1 逐次実行と並行実行

次のコードを検討してください。

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');
  });

このように .then() を使用すると、Promiseベースの関数が逐次に実行されます。asyncFunc1() の結果が確定した後でのみ、asyncFunc2() が実行されます。

Promise.all() は、Promiseベースの関数をより並行に実行するのに役立ちます。

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

40.6.2 並行処理のヒント:操作が開始されるときに焦点を当てる

非同期コードがどの程度「並行」であるかを判断するためのヒント:Promiseがどのように処理されるかではなく、非同期操作がいつ開始されるかに焦点を当ててください。

たとえば、次の各関数は asyncFunc1()asyncFunc2() をほぼ同時に開始するため、並行に実行します。

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

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

一方、次の両方の関数は asyncFunc1()asyncFunc2() を逐次的に実行します。asyncFunc2() は、asyncFunc1() のPromiseがフルフィルされた後にのみ呼び出されます。

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() はフォークジョイン

Promise.all() は、並行処理パターン「フォークジョイン」と緩やかに関連しています。以前に遭遇した例を再確認してみましょう。

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 Promiseを連鎖させるためのヒント

このセクションでは、Promiseを連鎖させるためのヒントを示します。

40.7.1 連鎖の誤り:末尾を失う

問題

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

  return promise;
}

計算は asyncFunc() によって返されるPromiseから始まります。しかし、その後、計算は続き、.then() を介して別のPromiseが作成されます。foo() は前者のPromiseを返しますが、後者を返す必要があります。これを修正する方法は次のとおりです。

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

40.7.2 連鎖の誤り:ネスト

問題

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

A行の .then() はネストしています。フラットな構造の方が優れています。

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

40.7.3 連鎖の誤り:必要以上のネスト

これは、回避可能なネストの別の例です。

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

ここでも、フラットな構造を得ることができます。

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

40.7.4 すべてのネストが悪いわけではない

次のコードでは、実際にネストから恩恵を受けています。

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)
      });
  })

A行で非同期の結果を受け取っています。B行では、コールバック内とC行で変数 connection にアクセスできるようにネストしています。

40.7.5 連鎖の誤り:連鎖する代わりにPromiseを作成する

問題

// 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);
        })
    });
  }
  // ···
}

A行では、db.insert() の結果を配信するためのPromiseを作成しています。これは不必要に冗長であり、簡略化できます。

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

重要なのは、Promiseを作成する必要がないということです。.then() 呼び出しの結果を返すことができます。追加の利点は、db.insert() の失敗をキャッチして再度リジェクトする必要がないことです。そのリジェクトを .insertInto() の呼び出し元に渡すだけです。

40.8 簡単なリファレンス:Promiseコンビネータ関数

特に記載がない限り、この機能はECMAScript 6(Promiseが言語に追加されたとき)で導入されました。

用語集

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>

これはAggregateErrorの型シグネチャです(いくつかのメンバは省略されています)

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>>>

これはSettlementObjectの型シグネチャです

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

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

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