Promise.resolve(): 指定された値で履行されたPromiseを作成Promise.reject(): 指定された値で拒否されたPromiseを作成.then() コールバックでの返り値と例外のスロー.catch() とそのコールバック.finally() [ES2018]XMLHttpRequest のPromise化util.promisify()Promise.all()Promise.race()Promise.any() と AggregateError [ES2021]Promise.allSettled() [ES2020]Promise.all() (上級)Promise.all() はフォークジョインPromise.all()Promise.race()Promise.any() [ES2021]Promise.allSettled() [ES2020] 推奨される読書
この章は、JavaScriptにおける非同期プログラミングの背景について、前の章に基づいています。
Promiseは、非同期的に結果を配信するための手法です。
以下のコードは、Promiseベースの関数 addAsync()(その実装は後で示します)を使用する例です。
addAsync(3, 4)
.then(result => { // success
assert.equal(result, 7);
})
.catch(error => { // failure
assert.fail(error);
});Promiseはイベントパターンに似ています。コールバックを登録するオブジェクト(Promise)があります。
.then()メソッドは、結果を処理するコールバックを登録します。.catch()メソッドは、エラーを処理するコールバックを登録します。Promiseベースの関数はPromiseを返し、結果またはエラー(完了した場合)を送信します。Promiseはそれを関連するコールバックに渡します。
イベントパターンとは対照的に、Promiseは1回限りの結果に最適化されています。
.then() と .catch() はどちらもPromiseを返すため、チェーンすることができます。これにより、複数の非同期関数を順次呼び出すのに役立ちます。詳細については後述します。Promiseとは何でしょうか?それを見るには2つの方法があります。
これは、2つの数値 x と y を加算する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つの関数が提供されます。
resolve は、結果を配信するために使用されます(成功した場合)。reject は、エラーを配信するために使用されます(失敗した場合)。図 22 は、Promiseがなりうる3つの状態を示しています。Promiseは1回限りの結果に特化しており、競合状態(早すぎるまたは遅すぎる登録)から私たちを保護します。
.then() コールバックまたは .catch() コールバックを早すぎるタイミングで登録すると、Promiseが確定したときに通知されます。.then() または .catch() が確定後に呼び出された場合、キャッシュされた値を受け取ります。さらに、Promiseが確定すると、その状態と確定値はもう変更できません。これにより、コードの予測可能性が高まり、Promiseの1回限りの性質が強制されます。
確定しないPromiseもある
Promiseがまったく確定しない可能性があります。例えば
new Promise(() => {})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を返すためです。
Promise.reject(): 指定された値で拒否されたPromiseを作成Promise.reject(err) は、値 err で拒否されるPromiseを作成します。
const myError = new Error('My error!');
Promise.reject(myError)
.catch(err => {
assert.equal(err, myError);
});.then() コールバックでの返り値と例外のスロー.then() はPromiseの履行を処理します。また、新しいPromiseを返します。そのPromiseがどのように確定するかは、コールバック内で何が起こるかによって異なります。3つの一般的なケースを見てみましょう。
まず、コールバックはPromiseでない値を返すことができます(A行)。その結果、.then() によって返されるPromiseは、その値で履行されます(B行で確認したように)。
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
})
.then(str2 => {
assert.equal(str2, 'abcabc'); // (B)
});次に、コールバックは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 => {
/*···*/
});
});3番目に、コールバックは例外をスローできます。その結果、.then() によって返されるPromiseは、その例外で拒否されます。つまり、同期エラーは非同期エラーに変換されます。
const myError = new Error('My error!');
Promise.resolve('abc')
.then(str => {
throw myError;
})
.catch(err => {
assert.equal(err, myError);
});.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');
});.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() は同期セミコロンの非同期バージョンです。
.then() は、2つの非同期操作を順次実行します。また、.catch() をミックスに追加して、複数のエラーソースを同時に処理させることができます。
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
})
.then(result2 => {
// ···
})
.catch(error => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
});.finally() [ES2018]Promiseメソッド .finally() は、多くの場合、次のように使用されます。
somePromise
.then((result) => {
// ···
})
.catch((error) => {
// ···
})
.finally(() => {
// ···
})
;.finally() コールバックは、somePromise および .then() や .catch() によって返される値に関係なく、常に実行されます。対照的に
.then() コールバックは、somePromise が履行された場合にのみ実行されます。.catch() コールバックは、以下の場合にのみ実行されます。somePromise が拒否された場合、.then() コールバックが拒否されたPromiseを返した場合、.then() コールバックが例外をスローした場合。.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)');
});.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();
});.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'以下は、1 回限りの結果を処理する場合に、プレーンなコールバックに対する Promise の利点の一部です。
Promise ベースの関数とメソッドの型シグネチャはより明確です。関数がコールバックベースの場合、一部のパラメーターは入力に関するものであり、最後にある 1 つまたは 2 つのコールバックは出力に関するものです。Promise の場合、出力関連のものはすべて、返された値を介して処理されます。
非同期処理ステップのチェーン化がより便利になります。
Promise は、(拒否による)非同期エラーと、(new Promise()、.then()、および .catch() のコールバック内で)例外が拒否に変換される同期エラーの両方を処理します。対照的に、非同期処理にコールバックを使用する場合、例外は通常は処理されません。自分で処理する必要があります。
Promise は、相互に互換性のないいくつかの代替手段を徐々に置き換えている単一の標準です。たとえば、Node.js では、多くの関数が Promise ベースのバージョンで利用できるようになりました。また、新しい非同期ブラウザー API は通常、Promise ベースです。
Promise の最大の利点の 1 つは、直接操作する必要がないことです。Promise は、非同期計算を実行するための同期のように見える構文である非同期関数の基礎です。非同期関数については、次の章で説明します。
Promise の動作を見ることは、Promise を理解するのに役立ちます。例を見てみましょう。
JSON データが含まれている次のテキストファイル person.json を検討してください。
{
"first": "Jane",
"last": "Doe"
}このファイルを読み取り、オブジェクトに解析するコードの 2 つのバージョンを見てみましょう。最初に、コールバックベースのバージョン。次に、Promise ベースのバージョンです。
次のコードは、このファイルの内容を読み取り、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 行の if は fs.readFile() によって報告された非同期エラーを処理し、B 行の try は JSON.parse() によって報告された同期エラーを処理します。
次のコードでは、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() コールバック内の例外が拒否になるためです。
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
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()
util.promisify() の使用: exercises/promises/read_file_async_exrc.mjsutil.promisify() の自分で実装: exercises/promises/my_promisify_test.mjsすべての最新のブラウザーは、データをダウンロードするための新しい 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
関数とメソッドを実装するためのルール
(非同期)拒否と(同期)例外を混同しないでください。
これにより、常に単一のエラー処理メカニズムに集中できるため、同期コードと非同期コードの予測可能性が高まり、シンプルになります。
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 => {
// ···
});
}ほとんどの 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 行)。
このアプローチの利点
同期的に開始すると、Promise ベースの関数が開始される順序に依存できるため、競合状態を回避できます。ファイルにテキストが書き込まれ、競合状態が回避される例が次の章にあります。
Promise が完了する前に、イベントループを実行できる中断が常に存在するため、Promise をチェーンしても他のタスクの処理時間が不足することはありません。
Promise ベースの関数は常に結果を非同期的に返します。同期的な戻り値がないことを確信できます。この種の予測可能性により、コードが扱いやすくなります。
このアプローチに関する詳細情報
コンビネーターパターンは、構造を構築するための関数型プログラミングのパターンです。これは、2 種類の関数に基づいています
JavaScript Promise に関しては
プリミティブ関数には、Promise.resolve()、Promise.reject() が含まれます
コンビネーターには、Promise.all()、Promise.race()、Promise.any()、Promise.allSettled() が含まれます。これらの各ケースで
次に、言及した Promise コンビネーターについて詳しく見ていきます。
Promise.all()これは Promise.all() の型シグネチャです
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>Promise.all() は、次のような Promise を返します。
promises が履行された場合は履行されます。promises の履行値を持つ配列です。これは、出力 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() がどのように機能するかを示しています。
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]);
});.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 を使用して、テキストファイルを文字列としてダウンロードします
response を取得します(A 行)。response.ok (B 行) は、「ファイルが見つかりません」などのエラーがあったかどうかを確認します。.text() (C 行) を使用して、ファイルの内容を文字列として取得します。次の例では、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!']
));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 回しか完了できないため、もう一方の結果を変更することはできません。
Promise.race()これは Promise.race() の型シグネチャです
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>Promise.race() は、promises の中の最初の Promise p が完了するとすぐに完了する Promise q を返します。q は p と同じ完了値を持っています。
次のデモでは、履行された 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() がどのように機能するかを示しています。
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を返します。
promisetimeoutInMsミリ秒後にリジェクトされるPromise2番目の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を生成した非同期操作を停止させるわけではありません。
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行のいずれかで確定します。一度確定すると、その確定値を変更することはできません。
Promise.any() と AggregateError [ES2021]これは Promise.any() の型シグネチャです。
Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>Promise.any() はPromise p を返します。どのように確定するかは、パラメータ promises (Promiseのイテラブルを参照)によって異なります。
p はそのPromiseで解決されます。p はすべてのリジェクト値を格納した AggregateError のインスタンスでリジェクトされます。これは 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() の動作を示しています。
これは、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']
));Promise.any() vs. Promise.all()Promise.any() と Promise.all() を比較するには2つの方法があります。
Promise.all(): 最初の入力リジェクトが結果のPromiseをリジェクトするか、そのフルフィルメント値は入力フルフィルメント値を持つ配列です。Promise.any(): 最初の入力フルフィルメントが結果のPromiseをフルフィルするか、そのリジェクト値は入力リジェクト値を持つ配列です(エラーオブジェクト内)。Promise.all() はすべてのフルフィルメントに関心があります。反対の場合(少なくとも1つのリジェクト)は、リジェクトにつながります。Promise.any() は最初のフルフィルメントに関心があります。反対の場合(リジェクトのみ)は、リジェクトにつながります。Promise.any() vs. Promise.race()Promise.any() と Promise.race() も関連していますが、関心のあるものが異なります。
Promise.race() は確定に関心があります。最初に確定したPromiseが「勝ち」ます。言い換えれば、最初に終了する非同期計算を知りたいということです。Promise.any() はフルフィルメントに関心があります。最初にフルフィルされたPromiseが「勝ち」ます。言い換えれば、最初に成功する非同期計算を知りたいということです。.race() の主な、比較的まれなユースケースは、Promiseをタイムアウトさせることです。.any() のユースケースはより広範です。次にそれらを見ていきます。
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');
}Promise.any() を実装するのか?Promise.any() の簡単な実装は、基本的に Promise.all() の実装のミラーバージョンです。
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 に対応します。
p がフルフィルメント値 v でフルフィルされた場合、e は次のようになります。
{ status: 'fulfilled', value: v }p がリジェクト値 r でリジェクトされた場合、e は次のようになります。
{ status: 'rejected', reason: r }promises の反復処理中にエラーが発生しない限り、出力Promise out がリジェクトされることはありません。
図26は、Promise.allSettled() の動作を示しています。
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' },
]));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'),
},
]
));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);
});
}Promiseコンビネータの場合、ショートサーキットとは、出力Promiseがすべての入力Promiseが確定する前に早期に確定することを意味します。次のコンビネータはショートサーキットします。
Promise.all():入力Promiseの1つがリジェクトされると、出力Promiseはすぐにリジェクトされます。Promise.race():入力Promiseの1つが確定すると、出力Promiseはすぐに確定します。Promise.any():入力Promiseの1つがフルフィルされると、出力Promiseはすぐにフルフィルされます。繰り返しますが、早期に確定しても、無視されたPromiseの背後にある操作が停止されるわけではありません。単にそれらの確定が無視されるだけです。
Promise.all() (高度)次のコードを検討してください。
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']);
});非同期コードがどの程度「並行」であるかを判断するためのヒント: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]);
}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!']
));このセクションでは、Promiseを連鎖させるためのヒントを示します。
問題
// 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 => {
// ···
});
}問題
// Don’t do this
asyncFunc1()
.then(result1 => {
return asyncFunc2()
.then(result2 => { // (A)
// ···
});
});A行の .then() はネストしています。フラットな構造の方が優れています。
asyncFunc1()
.then(result1 => {
return asyncFunc2();
})
.then(result2 => {
// ···
});これは、回避可能なネストの別の例です。
// 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;
});次のコードでは、実際にネストから恩恵を受けています。
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 にアクセスできるようにネストしています。
問題
// 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() の呼び出し元に渡すだけです。
特に記載がない限り、この機能はECMAScript 6(Promiseが言語に追加されたとき)で導入されました。
用語集
Promise.all()Promise.all<T>(promises: Iterable<Promise<T>>)
: Promise<Array<T>>P のフルフィルメント:すべての入力Promiseがフルフィルされた場合。P のリジェクト:1つの入力Promiseがリジェクトされた場合。Promise.race()Promise.race<T>(promises: Iterable<Promise<T>>)
: Promise<T>P の確定:最初の入力Promiseが確定した場合。Promise.any() [ES2021]Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>Pの成功: いずれかの入力Promiseが成功した場合。Pの失敗: すべての入力Promiseが失敗した場合。AggregateError。これはAggregateErrorの型シグネチャです(いくつかのメンバは省略されています)
class AggregateError {
constructor(errors: Iterable<any>, message: string);
get errors(): Array<any>;
get message(): string;
}Promise.allSettled() [ES2020]Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>Pの成功: すべての入力Promiseが確定した場合。Pの失敗: 入力Promiseの反復処理中にエラーが発生した場合。これはSettlementObjectの型シグネチャです
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}