コールバック地獄とは

JavaScript の非同期処理を素朴に書くと、コールバック関数が入れ子になっていきます。たとえばユーザー情報を取得し、その ID を使って投稿一覧を取得し、さらに最初の投稿のコメントを取得する、という3段階の処理を書いてみましょう。

getUser(userId, function (user) {
  getPosts(user.id, function (posts) {
    getComments(posts[0].id, function (comments) {
      console.log(comments);
    }, function (err) {
      console.error('コメント取得失敗:', err);
    });
  }, function (err) {
    console.error('投稿取得失敗:', err);
  });
}, function (err) {
  console.error('ユーザー取得失敗:', err);
});

ネストが右へ右へと深くなり、エラー処理が各段階に散らばります。この構造をコールバック地獄と呼びます。読みにくいだけでなく、エラーを見落としやすく、途中の処理を変更するとすぐ壊れてしまいます。

Promise の登場

この問題を解決するために登場したのが Promise です。Promise は「非同期処理の結果を箱に入れて渡す」オブジェクトと考えるとわかりやすいです。今すぐ結果がなくても、箱だけ先に受け取っておけます。箱が開いたとき(非同期処理が完了したとき)に何をするかを .then() で指定しておきます。

Promise の 3 つの状態

Promise オブジェクトは常に次の 3 状態のどれかにある。

状態 意味
pending 処理中。まだ結果が出ていない
fulfilled 処理成功。結果の値が確定した
rejected 処理失敗。エラーの理由が確定した

pending から fulfilled または rejected への遷移は一方向で、一度確定した状態は変わりません。「箱が開いたら中身は変わらない」とイメージすると掴みやすいです。

Promise の状態遷移

.then().catch() でチェーンする

Promise を使うと、コールバック地獄の例はこう書き直せる。

getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(err => console.error('取得失敗:', err));

ネストがなくなり、処理が上から下へ一直線に流れます。.then() は fulfilled になった値を受け取って次の Promise を返します。この連結をPromise チェーンと呼びます。エラーは .catch() を1か所に書くだけで、チェーンのどこで起きた失敗も拾えます。rejected になった Promise は .catch() まで伝播するためです。

.finally()

成否に関わらず必ず実行したい処理には .finally() を使います。ローディングスピナーを消す、一時ファイルを削除するといったクリーンアップ処理がその典型です。

showSpinner();

getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => console.log(posts))
  .catch(err => console.error(err))
  .finally(() => hideSpinner());

.finally() は値を受け取らず、戻り値も無視されます。「後片付けだけをする」という役割に徹しています。

まとめ

  • コールバック地獄はネストと分散したエラー処理が招く構造の問題
  • Promise は「非同期処理の結果を箱に入れて渡す」オブジェクト
  • 状態は pendingfulfilled / rejected の一方向遷移
  • .then() チェーンで処理をフラットに並べられる
  • .catch() はチェーン全体のエラーを1か所で受け取る
  • .finally() は成否に関わらず実行されるクリーンアップ用