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.then()は、promiseがresolveすると呼び出されますpromise.catch()は、promiseがrejectすると呼び出されますpromise.finally()は、promiseがresolveまたはrejectすると呼び出されますすべてのプロミスは「thenable」です。
つまり、元のプロミスがresolveすると実行されるthenという関数を利用できるということです。
promise.then(onResolved)とすると、コールバックonResolvedは元のプロミスがresolveされた値を受け取ります。
これは常に_新しい_「チェーンされた」プロミスを返します。
thenからvalueを返すと、「チェーンされた」プロミスはresolveされます。
thenの中でreasonをスローすると、「チェーンされた」プロミスは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;
});
これは約1000ms後に"Success!"をログに出力します。
promise1の状態と値はresolvedと"Success!"になります。
promise2の状態と値はresolvedとtrueになります。
元のプロミスがrejectしたときに実行される2つ目の引数も利用できます。
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したときに約1000ms後に来るresolveは黙って無視されることを理解しておくことが大切です。一度rejectまたはresolveすると、内部状態は変えられないからです。
プロミスから値を返すとそのプロミスはresolveされ、値をスローするとrejectされることも理解しておくことが大切です。
promise1がresolveし、チェーンされたonResolved(then(onResolved))がある場合、その続きは新しいプロミスで、resolveまたはrejectできます。
promise1がrejectし、チェーンされたonRejected(then(, onRejected))がある場合、その続きは新しいプロミスで、resolveまたはrejectできます。
エラーを捕捉して、元のプロミスがrejectしたときだけ続行したい場合もあります。
promise.catch(onCatch)とすると、コールバックonCatchは元のプロミスがrejectされた理由を受け取ります。
これは常に_新しい_「チェーンされた」プロミスを返します。
catchからvalueを返すと、「チェーンされた」プロミスはresolveされます。
catchの中でreasonをスローすると、「チェーンされた」プロミスは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);
約2分の1のケースでは、約1000ms後に"Success!"をログに出力します。
残りの約2分の1のケースでは、すぐに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からvalueを返すと、元のプロミスの状態と値がコピーされ、valueは無視されます。
finallyの中でreasonをスローすると、「チェーンされた」プロミスは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のチュートリアルが良い学習資料になります。
この演習では、無料会員に基本的な翻訳サービスを、プレミアム会員に品質保証付きの高度な翻訳を提供するTranslationServiceを作成します。
API
宇宙にある翻訳APIを見つけました。これはどんな翻訳のrequestも手頃な時間で処理してくれます。
これを活用したいところです。
宇宙の翻訳者たちはとても気まぐれで、無駄を嫌います。そこで、彼らを煩わせることなく過去の翻訳をfetchできる_API storage_の衛星も用意してくれています。
翻訳の取得
api.fetch(text)は、_API storage_からtextの翻訳を取得し、2つの値を提供するpromiseを返します。
translation: 実際の翻訳quality: 数値で表される品質_API storage_に翻訳が見つからない場合、APIはNotAvailableエラーを投げます。
翻訳はapi.requestメソッドを使って追加できます。
'text'が翻訳できない場合、APIはUntranslatableエラーを投げます。
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
翻訳のリクエスト
必ず存在するはずの翻訳でも、まだ_API storage_に追加されていないことがあります。これがNotAvailable(ストレージにはないが、リクエストできる)とUntranslatable(翻訳できない)の違いです。
api.request(text, callback)は、textの翻訳を実行して_API storage_に追加するようリクエストします。
完了すると、callback関数が呼び出されます。
callbackにはundefinedが渡されます。これは翻訳が成功し、api.fetchメソッドで取得できることを示します。callbackにはerrorが渡されます。これは何か問題が起きたことを示します。
宇宙のAPIは_不安定_なので、しばしば失敗します。
そうなった場合は、もう一度api.requestしてかまいません。api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
⚠ 警告! ⚠
APIは、requestが届くとさまざまな翻訳者をテレポートさせて魔法のような処理を行います。
これは非常にコストのかかる操作なので、翻訳が利用できるときには呼び出すべきではありません。
残念ながら、誰もがマニュアルを読むわけではないので、悪質な利用者を追い出す仕組みが用意されています。
利用可能なtextに対してapi.requestを呼び出すと、APIはその呼び出しでAbusiveClientErrorを投げ、それ以降のすべての呼び出しでも投げます。
すでに翻訳済みのものに対しては、決して翻訳をリクエストしないようにしてください。
無料サービスは、現在_API storage_にある翻訳だけを提供します。
無料会員に、_API storage_にすでにある翻訳を提供するメソッドfree(text)を実装します。
品質は無視し、APIが投げるエラーはそのまま伝えます。
api.fetchメソッドを使います(api.fetchはpromiseを返します)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
無料会員向けに、無料サービスを使ってテキストの配列を翻訳するメソッドbatch([text, text, ...])を実装します。すべての翻訳を返すか、1つのエラーを返します。
BatchIsEmptyエラーでrejectしますservice.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
_API storage_に翻訳を追加するよう_リクエスト_する、プレミアム会員向けのメソッドrequest(text)を実装します。
リクエストは、失敗すると自動的に再試行する必要があります。
同じリクエストに対して3回までしか呼び出してはいけません(宇宙の翻訳者を怒らせないでください!!!)。
api.requestがエラーを返さない場合は、undefinedでresolveしますapi.requestがエラーを返す場合は、最大2回再試行しますservice.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
翻訳を取得するプレミアム会員向けのメソッドpremium(text, quality)を実装します。
翻訳がNotAvailableの場合は、翻訳をリクエストし、_API storage_に追加されたあとに取得します。
このメソッドは、一定のqualityしきい値を満たす場合にのみ翻訳を返すようにします。
api.fetchがresolveした場合は、resolveする前に品質を確認しますapi.fetchがrejectした場合は、代わりに翻訳を_リクエスト_しますapi.requestがrejectした場合は、エラーをそのまま伝えますservice.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
注意
'arlogh Qoylu'pu'?の正しい翻訳は**How many times has it been heard?**です。
Exercismに登録すれば、37個のコンセプト159個の演習、そして本物の人間によるメンタリングとともに、JavaScriptを学んでマスターできます。すべて無料です。