【JS応用】Promise でのエラーハンドリング

Promiseにおけるエラーハンドリングの極意:堅牢な非同期処理を構築するために

現代のJavaScript開発において、非同期処理は避けて通れない要素です。特にPromiseの普及により、コールバック地獄から解放された一方で、エラーハンドリングの設計はより慎重なアプローチが求められるようになりました。本記事では、単なる`.catch()`の使い方にとどまらず、実務レベルで求められる堅牢なエラーハンドリング戦略を詳細に解説します。

Promiseチェーンにおけるエラーの伝播メカニズム

Promiseにおけるエラーハンドリングの基本は「拒否(Rejection)の伝播」にあります。Promiseチェーン内で発生した例外や、明示的に`Promise.reject()`された値は、チェーンの途中にある成功用コールバック(`.then()`の第一引数)をすべてスキップし、最初に見つかった失敗用コールバック(`.catch()`)へと到達します。

この挙動は非常に強力ですが、注意が必要です。もしチェーンの途中でエラーをキャッチし、そのまま何も返さなかったり、単にログを出力して終了したりすると、その後の`.then()`が実行されてしまいます。これは、エラーが発生したにもかかわらず「成功した」と誤認される状態を作り出す危険性があります。

非同期エラーハンドリングの実践的パターン

実務では、単一のPromiseだけでなく、複数の非同期処理を組み合わせるケースがほとんどです。ここでは、特に重要な3つのパターンを紹介します。

1. 局所的なエラーハンドリング
特定の処理だけが失敗しても全体を止めない場合、その処理の直後に`.catch()`を配置します。これにより、エラーを握り潰す(あるいは代替値を返す)ことで、後続の処理を継続させることができます。

2. グローバルなエラーハンドリング
アプリケーション全体で発生する未捕捉のPromise拒否は、`unhandledrejection`イベントで監視可能です。これは最終的な安全策(セーフティネット)として機能しますが、これに依存しすぎる設計は避けるべきです。

3. async/awaitによるtry-catchの活用
現代のJavaScriptでは、Promiseチェーンよりも`async/await`構文が推奨されます。これにより、同期処理と同様の`try-catch`構文でエラーを捕捉できるため、可読性が大幅に向上します。

実務における実装サンプル

以下に、実務で頻出する「APIリクエストの失敗を適切に処理し、ユーザーにフィードバックを返す」ための実装例を示します。


/**
 * ユーザーデータを取得する関数
 * @param {string} userId
 */
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    
    // HTTPステータスコードによるエラーチェック
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    // ログ基盤への送信など
    console.error("データ取得に失敗しました:", error);
    
    // エラーを再スローするか、デフォルト値を返すかを選択する
    // ここでは呼び出し元にエラーを伝える方針とする
    throw new Error("ユーザー情報の読み込みに失敗しました。時間をおいて再試行してください。");
  }
}

// 呼び出し元の実装
async function initializeApp() {
  try {
    const user = await fetchUserData("123");
    renderUI(user);
  } catch (err) {
    // ユーザーへの通知UIを表示
    showErrorMessage(err.message);
  }
}

このコードのポイントは、`fetch` APIがネットワークエラー以外では例外を投げないという仕様を、`response.ok`の確認によって補完している点です。また、エラーをキャッチした後に別のエラーとして再スローすることで、呼び出し元がエラーのコンテキストを理解しやすくしています。

Promise.allとPromise.allSettledの使い分け

複数の非同期処理を並行して実行する場合、エラーハンドリングの戦略は大きく分かれます。

`Promise.all`は、一つでもPromiseが拒否されると、即座に全体が拒否されます。これは「すべてが成功しなければ意味がない」処理に適しています。一方で、一部の処理が失敗しても他の処理の結果は利用したい場合は、`Promise.allSettled`を使用すべきです。`Promise.allSettled`はすべての処理が完了するまで待ち、成功・失敗の結果を配列として返します。

実務では、UIのパーツごとにデータを取得する際、特定のパーツの読み込み失敗で画面全体をクラッシュさせないために、`Promise.allSettled`を積極的に採用するのが現代的なフロントエンド開発の定石です。

実務アドバイス:エラーハンドリングのベストプラクティス

1. エラーの型を定義する
JavaScriptでは何でも投げられますが、TypeScriptを使用している場合は`Error`クラスを継承した独自のカスタムエラークラスを作成しましょう。これにより、`instanceof`演算子を使ってエラーの種別に応じた柔軟な分岐が可能になります。

2. エラーを握りつぶさない
`.catch()`内で何も処理をせず、空の関数を渡すのは厳禁です。デバッグが極めて困難になります。必ずログ出力、監視サービスへの通知、あるいは適切なデフォルト値の返却を行ってください。

3. タイムアウト処理を導入する
ネットワークリクエストは永久に終わらない可能性があります。`AbortController`を使用して、一定時間でリクエストをキャンセルする仕組みを組み込むことが、堅牢なアプリケーションへの第一歩です。

4. ユーザー体験を考慮する
システム的なエラーをそのままユーザーに見せるのは避けましょう。「ネットワークエラーが発生しました」という表示よりも、「通信環境をご確認ください」といった、ユーザーが次に何をすべきかが分かるメッセージを提供することが重要です。

まとめ

Promiseのエラーハンドリングは、単に「エラーを止める」ことではなく、「エラーが発生した際にアプリケーションをどのように安全な状態へ導くか」という設計思想そのものです。

– チェーンの途中でエラーを適切にハンドリングし、予期せぬ後続処理の実行を防ぐ。
– `async/await`と`try-catch`を基本とし、可読性の高い非同期処理を書く。
– `Promise.all`と`Promise.allSettled`を要件に応じて使い分ける。
– カスタムエラークラスを活用し、型安全かつコンテキストに基づいたエラー管理を行う。

これらの技術を習得し、適切に運用することで、フロントエンドアプリケーションの信頼性は飛躍的に向上します。非同期処理のエラーハンドリングを制する者は、プロダクトの品質を制すると言っても過言ではありません。日々の開発において、常に「この処理が失敗したとき、ユーザーはどうなるのか?」という問いを忘れずに設計を行ってください。

コメント

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