Promiseオブジェクトは、非同期処理が最終的に完了すること(または失敗すること)と、その結果の値を表します。
これは多くの人にとって難しいトピックです。特に、完全に_同期_の言語でプログラミングをしてきた人にとってはそうでしょう。 もし圧倒されてしまったり、並行性と並列性についてもっと学びたいと思ったら、素晴らしいトーク「Concurrency is not parallelism」を観る(go.dev経由)かVimeoで直接観る、そしてスライドを読むのがおすすめです。
Promiseには3つの状態があります:
プロミスは、作成された時点ではpendingです。 将来のある時点で、_resolve_または_reject_されることがあります。 一度resolveまたはrejectされると、そのプロミスが再びresolveやrejectされることはなく、状態が変わることもありません。
言い換えると、次のようになります:
プロミスは、さまざまな方法でresolveできます:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
上の例で、valueにはエラー、undefined、null、あるいは別のプロミスなど、_どんなもの_でも指定できます。
通常は、エラーではない値でresolveするのが望ましいでしょう。
プロミスは、さまざまな方法でrejectできます:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
上の例で、reasonにはエラー、undefined、nullなど、_どんなもの_でも指定できます。
通常は、エラーでrejectするのが望ましいでしょう。
プロミスは、resolveまたはrejectしたあとに、後続のアクションへと_つなげる_ことができます。
promiseがresolveすると、promise.then()が呼び出されますpromiseがrejectすると、promise.catch()が呼び出されますpromiseがresolveまたはrejectすると、promise.finally()が呼び出されますすべてのプロミスは「thenable」です。
つまり、元のプロミスがresolveされると実行されるthenという関数が利用できます。
promise.then(onResolved)の場合、コールバックonResolvedは、元のプロミスがresolveされたときの値を受け取ります。
thenは常に_新しい_「チェーンされた」プロミスを返します。
thenからvalueを返すと、「チェーンされた」プロミスはresolveします。
thenでreasonをthrowすると、「チェーンされた」プロミスはrejectします。
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
これは約1000ミリ秒後に"Success!"をログに出力します。
promise1の状態と値はresolvedと"Success!"になります。
promise2の状態と値はresolvedとtrueになります。
2つ目の引数を渡すこともでき、これは元のプロミスがrejectしたときに実行されます。
promise.then(onResolved, onRejected)の場合、コールバックonResolvedは元のプロミスがresolveされたときの値を受け取るか、コールバックonRejectedはプロミスがrejectされた理由を受け取ります。
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
"Success!"をログに出力します。
promise1の状態と値はresolvedと"Success!"になります。promise2の状態と値はresolvedとtrueになります。"NOPE!"をログに出力します。
promise1の状態と値はrejectedとNope!になります。promise2の状態と値はresolvedとfalseになります。ライフサイクルのルール上、プロミスがrejectすると、約1000ミリ秒後にやってくるresolveは黙って無視されることを理解しておくことが大切です。一度rejectまたはresolveしたあとは、内部状態を変えられないからです。
プロミスから値を返すとそのプロミスはresolveし、値をthrowするとrejectすることも理解しておきましょう。
promise1がresolveし、チェーンされたonResolvedがある場合(then(onResolved))、その続きの処理は新しいプロミスで、resolveもrejectもできます。
promise1がrejectし、チェーンされたonRejectedがある場合(then(, onRejected))、その続きの処理は新しいプロミスで、resolveもrejectもできます。
元のプロミスがrejectしたときだけエラーを捕捉して処理を続けたい、ということもあるでしょう。
promise.catch(onCatch)の場合、コールバックonCatchは元のプロミスがrejectされた理由を受け取ります。
catchは常に_新しい_「チェーンされた」プロミスを返します。
catchからvalueを返すと、「チェーンされた」プロミスはresolveします。
catchでreasonをthrowすると、「チェーンされた」プロミスはrejectします。
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
約半数のケースでは、約1000ミリ秒後に"Success!"をログに出力します。
残り半数のケースでは、すぐに42をログに出力します。
promise1がresolveすると、catchはスキップされ、thenに進んで値がログに出力されます。
promise1の状態と値はresolvedと"Success!"になります。promise2の状態と値はresolvedと"done"になります。promise1がrejectすると、catchが実行され、ここで_値が返される_ためチェーンはresolvedになり、thenに進んで値がログに出力されます。
promise1の状態と値はrejectedと"Nope!"になります。promise2の状態と値はresolvedと"done"になります。プロミスがresolveするかrejectするかに関わらず、確定したあとにコードを実行したいこともあるでしょう。
promise.finally(onSettled)の場合、コールバックonSettledは何も受け取りません。
finallyは常に_新しい_「チェーンされた」プロミスを返します。
finallyからvalueを返すと、元のプロミスの状態と値がコピーされ、valueは無視されます。
finallyでreasonをthrowすると、「チェーンされた」プロミスはrejectし、元のプロミスの状態と値や理由は上書きされます。
いくつかのメソッドを組み合わせた例です:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
randomNumberが0-3の場合:
myPromiseは値2, 4, 6, or 8でresolveされますfinalPromiseは値'yay'でresolveされますSampled data: ...Promise completedrandomNumberが4-5の場合:
myPromiseは理由'Sampling did not result in a sample'でrejectされますfinalPromiseは理由Error('Sampling did not result in a sample')でrejectされますPromise completed"uncaught rejected promise: Error('Sampling did not result in a sample')"というログが出力されます上の例でわかるように、rejectは文字列を渡しても動作し、プロミスはErrorでrejectすることもできます。
プロミスのチェーンや一般的な使い方がよくわからない場合は、MDNのチュートリアルが良い参考資料になります。