PromiseはES2015から導入されたJavaScriptの言語機能で、非同期処理を表すオブジェクトです。基本的な使い方としては、まずfetch
などの非同期的な処理の結果としてPromiseオブジェクトが得られます。それに対してthen
メソッドなどを用いることで、非同期処理が終わった後の次の処理を登録できます。また、ES2017で追加されたasync/awaitの構文はPromiseを便利に扱うためのものです。これはJavaScriptに独特の概念ではなく、C#ではTask、RustではFutureなどと呼ばれているようです。
JavaScriptを使っている多くの方は、Promiseを多かれ少なかれ使ったことがあるでしょう。そして、Promiseには結果が2種類、すなわち成功と失敗があることをご存知かと思います。このように、Promiseには状態があります。成功した状態、失敗した状態の他に、処理待ちの状態(まだ成功か失敗か分からない状態)があるのも分かりますね。
この記事では、Promiseの状態に関する用語を説明します。具体的には、pending、fulfilled、rejected、settled、resolvedを扱います。仕様書でいうと25.6 Promise Objectsの冒頭に書いてある内容です。
背景: Promise.allSettled
いきなり用語が英語になりましたが、別に英語力で皆さんを圧倒したいわけではありません。この記事の背景には、Promise.allSettled
があります。これはECMAScriptのプロポーザルの1つで、2019年1月のTC39ミーティングでStage2に昇格しました。つまり、多分数年後くらいにはPromise.allSettled
がJavaScriptに追加される可能性が高いということです。
このPromise.allSettled
は、Promise.all
やPromise.race
の仲間です。引数にPromiseの配列を受け取り、全てのPromiseの結果が出るまで待つという動作をします。Promise.all
との違いは、渡されたPromiseのいずれかが失敗した場合に表れます。Promise.all
はその場合即座に全体が失敗するのに対し、Promise.allSettled
はその場合でも渡された全てのPromiseの結果が出るまで待ちます。詳細な動作は実装例を見てみると分かりやすいでしょう。Promise.allSettled
はおおよそ次のように実装することができます(まだプロポーザルなので、今後仕様が変化する可能性があります)。
Promise.allSettled = promises =>
Promise.all(
promises.map(p =>
p.then(
result => ({ status: "fulfilled", value: result }),
error => ({ status: "rejected", reason: error })
)
)
);
この例を見て分かる通り、Promise.allSettled
の結果は必ず成功となり、オブジェクトの配列を返します。各オブジェクトは渡されたそれぞれのPromiseの結果を表しており、成功の場合は{ status: "fulfilled", value: 値 }
という形のオブジェクトが、失敗の場合は{ status: "rejected", reason: エラーオブジェクト }
という形のオブジェクトが返ります。
実際に使ってみましょう。
Promise.allSettled([
fetch('/'),
fetch('https://qiita.com/'),
fetch('https://nonexistend.invalid/'),
]).then(console.log);
実行してみるとこんな感じになります(https://qiita.com/ で実行)。
Promise.allSettled
に3つのPromiseを渡した結果、配列が得られました。先ほど説明したように、それぞれのPromiseが成功したのか失敗したのかがstatus
プロパティに入っている文字列で判別可能になっています。
ここでのポイントは、status
プロパティに入っている"fulfilled"
または"rejected"
という文字列です。前者が成功したPromiseの結果、後者が失敗したPromiseの結果に対応します。このPromise.allSettled
を使いたいときに、いちいちどちらがどちらなのか調べるのはなんとも馬鹿らしいですね。最低限、この2つの用語は覚えておきたいものです。
直感的な説明
では、本題である用語の説明に入ります。まず直感的な説明をします。
fulfilled, rejected, settled
先ほど出てきたfulfilledとrejectedは、既に結果が出ているPromiseの結果を表す用語です。fulfilledが成功した状態、rejectedは失敗した状態です。
また、Promise.allSettled
の名前に使われているsettledというのは、fulfilledまたはrejectedである状態を指す言葉です。つまり、成功でも失敗でもいいからとにかく既に結果が出ているPromiseを指す言葉です。Promise.allSettled
というのは、全てのPromiseがsettledになるまで待つという意味だったのですね。
pending
pendingは、まだ結果が出ていない状態のことです。つまり、settledでないPromiseはpendingです。
よく見ると、上の画像(Promise.allSettled
の実行例)でPromise.allSettled
の返り値にPromise {<pending>}
と書いてありますね。これはPromise.allSettled
が実行された瞬間はまだ結果が出ていないということを意味しています。
明らかに、全てのPromiseはpending、fulfilled、rejectedのいずれかです。つまり、まだ結果が出ていないか、成功という結果が出ているか、失敗という結果が出ているかのいずれかということですね。
resolved
しかし、まだひとつ解決していない用語が残っていますね。それがresolvedです。以上の3つ(settledを含めると4つ)のほかに、全てのPromiseはresolvedであるかそうでない(unresolvedである)かに分類できます。
直感的には、resolvedなPromiseというのは、既に自身の処理を終えているPromiseのことです。必然的に、settledなPromiseはresolvedですが、pendingなPromiseはresolvedな場合とunresolvedな場合があります。
自分の処理が終わらないと結果が決まりませんから、settledなPromiseはresolvedであるというのは分かりますね。問題はpendingな場合です。ポイントは、Promiseというのは自身の結果を他のPromiseに委譲することができるという点です。AというPromiseがBというPromiseに結果を委譲したがBがまだpendingな場合、Aはresolvedだがpendingであることになります。コードで例を見てみましょう。
const sleep = duration => new Promise((resolve, reject) => setTimeout(resolve, duration));
const p1 = new Promise((resolve, reject) => {
resolve(sleep(3000).then(()=> console.log('hi')));
});
ここで定義したsleep
は引数で指定された時間の後に解決されるPromiseを返す関数です。注目していただきたいのはp1
です。ご存知のように、Promiseコンストラクタを用いてPromiseを作る場合、コンストラクタに関数をひとつ渡します。その関数はresolve
とreject
を受け取り、これを呼び出すことで作られたPromiseが解決(resolve)されます。resolve
に値を渡した場合はその値がPromiseの結果(成功)となりますが、上の例のようにresolve
に別のPromise(今回はsleep(3000).then(()=> console.log('hi'))
)を渡すことができます。これが委譲です。
つまり、sleep(3000).then(()=> console.log('hi'))
がfulfilledになると同時にp1
もfulfilledになります。今回の場合、p1
がfulfilledになるのは3秒後ですね。p1
は、作られると同時にresolvedになります。なぜなら、即座に別のPromiseに結果を委譲しているからです。しかし、委譲されたPromiseは3秒後までpendingなので、p1
も同様に3秒後までpendingです。3秒後に委譲先のPromiseがfulfilledになると同時にp1
もfulfilledになります。
なお、sleep(3000)
は3秒後までunresolvedです。なぜなら、resolve
が呼ばれるまでは結果が決まらないし委譲も行なわれないからです。お分かりのように、PromiseコンストラクタでPromiseを作る場合、resolve
またはreject
が呼ばれるまでがunresolvedな状態で、呼ぶとresolved(まだpendingのままかもしれないしsettledになるかもしれない)になります。
定義を見る
ここまで説明した用語は、冒頭でも触れたECMAScript仕様書に定義が載っています。
Any Promise object is in one of three mutually exclusive states: fulfilled, rejected, and pending:
A promise p
is fulfilled if p.then(f, r)
will immediately enqueue a Job to call the function f
.
A promise p
is rejected if p.then(f, r)
will immediately enqueue a Job to call the function r
.
A promise is pending if it is neither fulfilled nor rejected.
A promise is said to be settled if it is not pending, i.e. if it is either fulfilled or rejected.
A promise is resolved if it is settled or if it has been “locked in” to match the state of another promise. Attempting to resolve or reject a resolved promise has no effect. A promise is unresolved if it is not resolved. An unresolved promise is always in the pending state. A resolved promise may be pending, fulfilled or rejected.
つまり、おおよそ以下の定義となっています。
-
p.then(f, r)
がすぐにf
を呼び出すジョブを登録するならばp
はfulfilledである。 -
p.then(f, r)
がすぐにr
を呼び出すジョブを登録するならばp
はrejectedである。 - fulfilledでもrejectedでもないPromiseはpendingである。
- pendingでないPromiseはsettledである。
- settledなPromiseはresolvedである。また、状態が別のPromiseと一致するように“固定”されているPromiseもresolvedである。
特にfulfilledとrejectedの定義が特徴的です。「すぐにf
を呼び出すジョブを登録する」というのがまたややこしい概念ですが、「ジョブを登録」という言い方をしていることから想像がつくように、f
は同期的には呼び出されません。このことは次の例を実行してみると分かります。
Promise.resolve(3).then(console.log);
console.log('hi');
これを実行すると、
hi
3
という順番でログが表示されます。Promise.resolve(3)
はすでにfulfilledなPromiseを返しますから、それに対してthen
を呼び出すことで、console.log(3)
を呼び出すジョブがすぐに登録されます。しかし、そのジョブは同期的ではないため、次の文console.log('hi')
よりは後になります。今の実行が終わり次第、ジョブとして登録されたconsole.log(3)
が実行されるのです。
これがどれくらいすぐかというと、setTimeout(fn, 0)
とかよりもすぐです。
/*
* 3
* hi
* の順番で表示される
*/
Promise.resolve(3).then(console.log);
setTimeout(()=> console.log('hi'), 0);
非同期的に実行されたジョブの中からでも、すぐにジョブを登録すれば待っているタイマーに割り込むことができます。すぐというのはこれくらいすぐです。
/*
* 3
* 5
* hi
* の順番で表示される
*/
Promise.resolve(3).then(v=> {
Promise.resolve(5).then(console.log);
console.log(v);
});
setTimeout(()=> console.log('hi'), 0);
なお、仕様書的には一応、Promiseオブジェクトがfulfilled、rejected、pendingのどの状態であるかという情報を[[PromiseState]]内部スロットに保持しています(仕様書 25.6.6 Properties of Promise Instance)。実際、Promise.prototype.thenの定義を見ると、確かに[[PromiseState]]がfulfilledやrejectedの場合はすぐに当該関数を呼ぶという処理が書いてあります。
最後にクイズです。次のPromise p2
, p3
, p4
が生成された直後の状態を分類してみてください。
const p2 = new Promise(resolve => {
resolve(Promise.resolve(123))
});
const p3 = new Promise(()=> {});
const p4 = Promise.resolve(p3);
答えは、p2
がfulfilled、p3
がunresolved、p4
がresolvedかつpendingです。
まとめ
この記事ではPromiseの状態にまつわる用語であるfulfilled、rejected、pending、settled、そしてresolvedについて説明しました。Promise.allSettled
の追加が現実味を帯びてきた今、これらの用語は今後身近になってくると思われます。
Q&A
-
Q. 何で
Promise.fulfill
じゃなくてPromise.resolve
なの? -
A. Promiseオブジェクトを渡したらfulfilledじゃなくてrejectedになる可能性があるからです(多分)。一方、
Promise.resolve
の返り値のPromiseは必ずすでにresolvedです。 - Q. settledとかはいいとしても、長々と説明していたresolvedとか何の役に立つの?
- A. (目を逸らす)