【JS応用】Promise: then vs catch

Promiseにおけるthenとcatchの役割と非同期処理のベストプラクティス

JavaScriptの非同期処理において、Promiseは現代のアプリケーション開発における基盤です。特にthenメソッドとcatchメソッドの使い分けは、コードの堅牢性と保守性に直結します。多くの開発者がなんとなく「成功ならthen、失敗ならcatch」と理解していますが、プロフェッショナルな現場では、その挙動、戻り値の連鎖、およびエラーハンドリングのスコープを深く理解することが求められます。本記事では、Promiseのチェイニングにおける両者の関係性と、なぜcatchを適切に配置することが重要なのかを徹底的に解説します。

thenメソッドの真の姿と2つのコールバック

多くの開発者は、then(onFulfilled)という形式で成功時の処理のみを記述しがちですが、実はthenメソッドは第2引数としてonRejected(エラーハンドリング用コールバック)を受け取ることができます。

Promise.prototype.then(onFulfilled, onRejected)

この形式において、第2引数に渡された関数は、そのPromiseが拒否(rejected)された場合に実行されます。しかし、実務においてthenの第2引数を使うことは推奨されません。なぜなら、thenの第1引数(onFulfilled)内で発生したエラーを、同じthenの第2引数(onRejected)でキャッチすることはできないからです。エラーハンドリングの対象が「直前のPromiseの結果」に限定されるため、処理の追跡が困難になります。

catchメソッドの役割とエラー伝播のメカニズム

catchメソッドは、実質的にthen(null, onRejected)の糖衣構文です。しかし、その真価は「Promiseチェーン全体のエラーを一元管理できる」という点にあります。

Promiseチェーンにおいて、ある段階でエラーが発生すると、JavaScriptエンジンは後続の成功用コールバック(then)をすべてスキップし、最初に見つかったcatchメソッドまで制御をジャンプさせます。この「エラーの伝播(Propagation)」こそが、非同期処理を宣言的に記述するための要です。

catchは単なるエラーログ出力の場所ではありません。catchブロック自体も新しいPromiseを返します。つまり、catch内で新しい値を返せば、後続のthenは「エラーから復帰した後の正常系」として処理を継続できるのです。

サンプルコードによる挙動の比較

以下のコードは、thenの第2引数を使用した場合と、catchを使用した場合の挙動の差異を示しています。


// 悪い例:thenの第2引数でエラーを捉えようとする
promise
  .then(
    (data) => {
      throw new Error('処理中に発生したエラー');
    },
    (err) => {
      console.log('これはキャッチできない:', err.message);
    }
  )
  .then(() => console.log('正常に終了'));

// 良い例:catchでチェーン全体のエラーを一元管理する
promise
  .then((data) => {
    // 成功処理
    return processData(data);
  })
  .then((result) => {
    // 別の成功処理
    return saveResult(result);
  })
  .catch((err) => {
    // 上記のいずれか、あるいはプロセス全体でのエラーをここで捕捉
    console.error('チェーン全体のエラーハンドリング:', err);
    // 必要に応じてエラーを再スロー、またはフォールバック値を返す
    return defaultValue;
  })
  .then((val) => {
    console.log('エラーから復帰、あるいは正常終了後の処理:', val);
  });

実務におけるエラーハンドリング戦略

実務では、単にcatchを書くだけでは不十分です。「どこでエラーを止めるべきか」を常に意識する必要があります。

1. エラーの境界を明確にする
すべての非同期処理を一つの巨大なcatchで囲むのではなく、論理的な単位(例えばAPIリクエスト単位)でチェーンを区切り、それぞれに適切なcatchを配置することで、特定の機能が失敗した際にアプリケーション全体を停止させない設計が可能になります。

2. catch内での再スロー
catch内でエラーを握りつぶす(何も返さない、あるいはnullを返す)と、後続のthenは「成功」と見なして処理を開始します。これはバグの温床です。エラーをログに記録し、さらに上位の階層で処理を継続させたい場合は、catch内で必ず throw err を実行するか、Promise.reject(err) を返すべきです。

3. finallyの活用
処理の成功・失敗に関わらず、リソースの解放やローディングスピナーの非表示を行いたい場合は、thenやcatchの後にfinallyを使用します。これはチェーンの結果に関わらず実行されるため、クリーンアップ処理の重複を避けることができます。

Promiseチェーンのアンチパターン:ネストの罠

初心者がやりがちな「Promiseのネスト(コールバック地獄の再来)」は避けるべきです。thenの中にさらにthenを記述すると、コードの可読性が著しく低下し、エラーハンドリングも複雑化します。


// アンチパターン
getData().then(data => {
  getMoreData(data).then(moreData => {
    console.log(moreData);
  }).catch(err => {
    // ここで発生したエラーは外側のcatchに届かない可能性がある
  });
});

// ベストプラクティス:フラットなチェーンを維持する
getData()
  .then(data => getMoreData(data))
  .then(moreData => console.log(moreData))
  .catch(err => console.error(err));

async/awaitとの併用と未来の展望

現在のフロントエンド開発では、Promiseチェーンよりもasync/await構文が主流です。しかし、async/awaitはPromiseの抽象化に過ぎません。awaitはthenの成功を待機し、エラーが発生すれば例外をスローします。

この場合、catchの代わりにtry/catchブロックを使用します。しかし、複数の非同期処理を並列で実行する場合(Promise.allなど)は、依然としてPromiseの知識が不可欠です。Promise.allが一つでも拒否されれば、全体のcatchが呼び出されます。このような高度な並列制御を行う際にも、thenとcatchの振る舞いを理解していることが前提となります。

まとめ

Promiseにおけるthenとcatchの関係は、単なる成功と失敗の分岐ではありません。それは、非同期処理の実行パイプラインを構築し、エラーの伝播を制御するための強力なメカニズムです。

– thenの第2引数は避け、原則としてcatchを使用する。
– catchはチェーンのエラーを集約する場所であり、必要に応じて復帰処理を行う場所である。
– エラーを握りつぶすときは意図を持って行い、そうでない場合は再スローする。
– Promiseチェーンはフラットに保ち、ネストを避ける。

これらの原則を守ることで、複雑な非同期ロジックであっても、予測可能でメンテナンス性の高いコードを実現できます。フロントエンド・スペシャリストとして、常に「このエラーはどこで捕捉されるべきか?」を自問自答し、堅牢なアプリケーション設計を心がけてください。Promiseの理解を深めることは、JavaScriptをマスターするための最も重要なステップの一つです。

コメント

タイトルとURLをコピーしました