Menu
日本語

JavaScript Promise入門 | then/catch/Promise.allの使い方

JavaScriptのPromiseの仕組みを整理。3つの状態、then・catchでのチェーン、Promise.allでの並列処理、new Promiseで自作する方法まで解説します。

Promise は未来の値を入れる「箱」のようなもの

JavaScript でネットワーク通信やファイルの読み込み、タイマー処理など、時間がかかる処理を行うとき、結果をその場で返すことはできません。代わりに返ってくるのが Promise です。Promise は「いずれ手に入るはずの値」を表すオブジェクトだと考えてください。

index.js
Output
Click Run to see the output here.

最初の console.log では、Promise が pending(保留中)の状態で表示されます。0.5 秒後に Promise が解決され、.then のコールバックがその値を受け取って実行されるわけです。Promise そのものはただのオブジェクトにすぎませんが、値が届いたタイミングで「待っている側」に通知できる仕組みを持っている点がポイントです。

Promise の 3 つの状態

Promise は常に、次の 3 つのうちいずれかの状態にあります。

  • pending(保留中) — 処理がまだ進行中。値はまだありません。
  • fulfilled(成功) — 処理が成功し、値が得られた状態。
  • rejected(失敗) — 処理が失敗し、エラーが得られた状態。

Promise は pending から fulfilled または rejected のどちらかに 一度だけ 遷移し、その後はずっとその状態のままです。一度解決された Promise を元に戻したり、二重に解決したりすることはできません。

index.js
Output
Click Run to see the output here.

Promise.resolve(value) はすでに fulfilled 状態の Promise を作り、Promise.reject(error) はすでに rejected 状態の Promise を作ります。テストを書くときや、関数によっては値が即座に決まるケースで Promise を返したいときに便利です。

値の取り出し方:.then.catch の使い方

Promise の中身は直接取り出せません。代わりに .then にコールバックを渡しておくと、値の準備ができた時点で Promise 側がそのコールバックを呼んでくれます。

index.js
Output
Click Run to see the output here.

.catch(fn) は Promise が reject されたときに走ります。実は内部的には .then(undefined, fn) のシンタックスシュガーです。チェーンの末尾に .catch() を 1 つ置いておけば、そこまでの どのステップ で reject されてもまとめて拾えるので、.then ごとに付ける必要はありません。

Promise チェーン:.then は毎回新しい Promise を返す

ここが一番つまずきやすいポイントです。.then() は単にコールバックを実行するだけでなく、新しい Promise を返します。その Promise は、コールバックが返した値で resolve されます。だからこそチェーンがつながるわけです。

index.js
Output
Click Run to see the output here.

各ステップが次の処理へと値を渡していきます。.then のコールバックが Promise を返した場合、チェーンは その Promise の完了を待ってから次に進むので、非同期処理をきれいに組み立てられます。

index.js
Output
Click Run to see the output here.

非同期処理を3ステップ順番に、しかもネストなしで書けています。同じ処理をコールバックで書いたものと見比べれば、Promise がここまで普及した理由がすぐわかるはずです。

エラーはチェーンを伝って流れていく

reject された Promise は、.catch にたどり着くまで途中の .then をすべてスキップします。Promise のエラーハンドリングは、基本的にこれだけです。

index.js
Output
Click Run to see the output here.

.then の中で例外を投げると、その .then が返した Promise が reject されます。以降の .then はその reject をそのまま次へ受け流し、最終的に .catch が拾ってくれる、という流れです。基本的には、チェーンの末尾に .catch を1つ置いておけば十分です。逆に .catch が1つもないチェーンは「unhandled promise rejection」の警告が出るので、必ず対処しておきましょう。

new Promise で自作する書き方

普段は、ライブラリが返してくれる Promise を使うことがほとんどです。ただ、ごくたまに Promise を返してくれないもの——たとえば昔ながらのコールバック形式の API——を自分で Promise 化したい場面が出てきます。

index.js
Output
Click Run to see the output here.

new Promise に渡す関数は executor(実行関数) と呼ばれます。引数は2つで、resolve(成功時の値を渡して呼ぶ)と reject(エラーを渡して呼ぶ)です。どちらか一方をちょうど1回だけ呼んでください。2回目以降の呼び出しは無視されます。

覚えておくと後々ラクになるコツが2つあります。

  • new Promise を使うのは、まだ Promise 化されていないものをラップするときだけにしましょう。すでに Promise を返す関数なら、そのまま return すれば十分です。
  • reject には必ず文字列ではなく Error オブジェクトを渡しましょう。スタックトレースは残しておく価値があります。

並列実行には Promise.all を使う

.then でつなぐ Promise チェーンは順番に実行されます。独立した非同期処理が複数あって同時に走らせたいときは、Promise.all の出番です。

index.js
Output
Click Run to see the output here.

3つのタイマーはすべて並行して走ります。Promise.all は、すべての Promise が fulfilled になったタイミングで、入力と同じ順序の結果の配列を返してくれます。トータルの所要時間は900msではなく、だいたい400msほどです。

ただし注意点があります。Promise.all は、いずれかの Promise が reject された瞬間に全体として reject され、他の結果は捨てられてしまいます。「3つのAPI呼び出しがすべて揃わないとページを描画できない」といった、全部の結果が必須のケースでは、これが正しい挙動です。そうでない場合は allSettled を使いましょう。

一部失敗してもOKなとき: Promise.allSettled

Promise.allSettled は、fulfilled であれ rejected であれ、すべての Promise が完了するのを待ってから、結果のレポートをまとめて返してくれます。

index.js
Output
Click Run to see the output here.

それぞれの結果は { status: "fulfilled", value } または { status: "rejected", reason } というオブジェクトで返ってきます。一部が失敗しても処理を続けたい場面、たとえばイベントのバッチログ、サムネイルの一括取得、独立したヘルスチェックなどで便利です。

あわせて押さえておきたいコンビネータがあと2つあります。

  • Promise.race([...]) — 複数の Promise のうち、成功・失敗を問わず 最初 に決着したものの結果を返します。タイムアウト処理に便利です。
  • Promise.any([...]) — 最初に成功した Promise の値で fulfilled になり、reject は無視します。全部失敗したときだけ reject されます。

Promise は常に非同期で動く

たとえすでに resolve 済みの Promise であっても、.then のコールバックは必ず非同期で呼ばれます。同期的に実行されたり、同じティック内で走ったりすることはありません。

index.js
Output
Click Run to see the output here.

出力は 即座に の順になります。.then のコールバックは、現在実行中のコードが終わるのを待ってから、マイクロタスクキュー経由で実行されるからです。「Promise のコールバックは絶対に同期的には実行されない」——このルールがあるおかげで、Promise と同期コードを混ぜても挙動が読みやすくなります。同期コードが先に片付くことが保証されているわけですね。

次は async/await へ

.then をつないでいく書き方でも動きはしますが、ステップが3つ4つと増えてくると、どうしても階段状のネストになって見づらくなります。そこで登場するのが async/await。Promise の上に乗った構文で、非同期処理をまるで同期コードのように書けます。エラーは try/catch で拾えるし、途中の値も普通の変数に入れられる。次の章で詳しく見ていきましょう。

よくある質問

JavaScriptのPromiseとは何ですか?

Promiseは「まだ結果が出ていない値」を表すオブジェクトです。典型例はfetchなど非同期処理の将来の戻り値ですね。状態は常にpending(待機中)、fulfilled(成功)、rejected(失敗)のいずれか。最終的な値を受け取るには、.then().catch()でコールバックを登録します。

thenとcatchはどう違うの?

.then(onFulfilled)はPromiseが成功したときに呼ばれ、解決された値を受け取ります。一方.catch(onRejected)は、そのPromise(もしくはチェーンの上流のどこか)がrejectされたときに呼ばれ、エラーを受け取ります。チェーンの末尾に.catch()をひとつ置いておけば、上流のどの段階の失敗もまとめて拾えます。

Promise.allは何をしてくれる?

Promise.all([p1, p2, p3])はPromiseの配列を受け取り、全部がfulfilledになった時点で、解決値の配列としてまとめて返してくれます。ただしひとつでもrejectされると、その瞬間に全体が失敗扱いに。失敗したものがあっても結果を全部受け取りたいときはPromise.allSettledを使いましょう。

Promiseとasync/awaitはどちらを使うべき?

実は中身は同じで、async/awaitはPromiseの上に乗った糖衣構文です。新しく書くコードはasync/awaitのほうが読みやすいことが多いですが、戻り値はやはりPromiseですし、エラー処理はtry/catch.catch()、並列実行にはPromise.allという具合に結局Promiseを触ります。Promiseの仕組みを理解しておくとasync/awaitもすんなり腑に落ちますよ。

Coddyでコードを学ぼう

始める