こんにちは、Ryohei(@ityryohei)です!

堅牢なWebアプリケーションを開発する上で、エラーハンドリング(例外処理)は避けて通れないテーマです。エラーを適切に処理しないと、アプリケーションが突然停止したり、ユーザーに不快な体験を与えたりする原因になります。

モダンなJavaScript開発では、非同期処理(API通信など)が主流となり、エラーの発生源が複雑化しています。単なるtry...catchだけでなく、Promiseやasync/awaitでのエラーを「漏らさず」「正しく」捕捉し、さらにブラウザ全体で未処理のエラーを監視するグローバルハンドリングのテクニックは、バグのないアプリケーションを構築するための必須スキルです。

本記事では、同期・非同期処理におけるエラー捕捉のメカニズムの違いを明確にし、実務で必須となるエラー伝播の設計戦略、そして未捕捉エラーを検出するグローバル監視までを徹底解説します。


非同期処理でエラーが起きたときって、どこでキャッチすればいいの?それとも、ブラウザ全体でまとめて監視する方法があるの?

上記の疑問にお答えします。

1. 基礎知識:同期処理のエラー捕捉メカニズム

エラー処理の基本は、コールスタックを遡る(さかのぼる)仕組みを理解することです。

1.1. try...catchと例外の伝播

同期処理においてtry...catch内でエラー(例外)が発生すると、JavaScriptエンジンはただちに実行を中断し、コールスタックを逆順に辿りながら、エラーを捕捉できる最も近いcatchブロックを探します。

  • try: エラーが発生する可能性のあるコードを囲みます。
  • catch (error): 例外を捕捉し、エラーオブジェクトを受け取って処理を再開します。
  • finally: trycatchの結果にかかわらず、必ず実行されます。(リソースの解放などに使われます。)

1.2. エラーオブジェクト(Error Object)の活用

catchブロックで受け取るerrorオブジェクトは、単なるメッセージではありません。エラーオブジェクトは以下の重要な情報を含んでいます。

  • name:エラーの種類(例: SyntaxError, TypeError, ReferenceError)。
  • message:エラーの具体的な説明。
  • stack:エラーが発生したコールスタックの履歴。デバッグ時に最も重要な情報です。

2. 非同期処理の壁:エラーハンドリングの「設計戦略」

非同期処理のエラーは、メインのコードフロー(コールスタック)とは別のタイミングで発生するため、同期的なtry...catchでは捕捉できません。Promiseは、この問題を解決するために「失敗」を専門に扱う機構を提供します。

2.1. Promiseの状態遷移と.catch()の役割

Promiseは「成功(Fulfilled)」または「失敗(Rejected)」のどちらか一方に遷移します。エラーハンドリングは、Promiseが失敗状態(Rejected)になったときに発動します。

状態意味処理
Pending処理中初期状態
Fulfilled成功.then() で処理
Rejected失敗.catch() で処理
  • エラー伝播の原則: .catch()でエラーを処理した後、returnを省略するか、別のエラーをthrowしない限り、その後の.then()には正常な値(またはundefined)が渡り、チェーンが途切れません。

2.2. async/awaitと同期的なエラー捕捉の復活

async/awaitは、Promiseの失敗を「同期的な例外」として扱うよう抽象化します。

  • awaitで待機しているPromiseが失敗した場合、その箇所で例外がthrowされたのと同じ挙動になります。
  • これにより、非同期コードを同期処理と全く同じようにtry...catchで囲むことが可能になり、コードがシンプルになります。

JavaScript

async function loadData() {
  try {
    const data = await fetchData('/api/data'); 
    return data;
  } catch (error) {
    // 捕捉したエラーをログに記録し、さらに呼び出し元へ再スローする
    console.error("データ取得に失敗:", error.message);
    throw error; // 呼び出し元で再度キャッチできるようにエラーを伝播させる
  }
}

2.3. 並列処理のエラー管理:Promise.all() vs. Promise.allSettled()

実務では複数のAPIを同時に呼び出すことが多いため、エラー処理の戦略的な使い分けが重要です。

処理方法目的エラー発生時の挙動最適な利用シーン
Promise.all()すべてのタスクが成功することを保証一つでも失敗すると全体が即座に失敗(ショートサーキット)し、最初に発生したエラーのみを返す。厳密な依存関係がある、すべてが成功しないと意味がない処理。
Promise.allSettled()すべてのタスクの結果を知りたいすべてのPromiseが完了(成功/失敗)するのを待つ。個々の結果を配列で返す。必須ではないタスクが含まれる、部分的な失敗を許容する処理。

3. 最重要課題:未捕捉エラー(Uncaught Exception)のグローバル監視

Promiseチェーンの最後に.catch()がない、あるいはasync関数内でtry...catchが漏れた場合、エラーはアプリケーションのどこにも捕捉されず、ブラウザ全体に未捕捉エラーとして露出します。

これはWebアプリの最も深刻な問題であり、ユーザー体験の低下やデータの喪失に直結します。

3.1. window.onerrorによる同期エラーの監視

HTMLのDOM操作や同期的なスクリプトで発生した未捕捉エラーは、window.onerrorイベントで監視できます。

JavaScript

window.onerror = function(message, source, lineno, colno, error) {
  console.log("【グローバル監視: 同期エラー】", message);
  
  // 外部のエラー追跡サービス(Sentry, Datadogなど)に報告
  reportErrorToServer(error); 
  
  // trueを返すと、ブラウザコンソールへの標準エラー出力を抑制できる
  return true; 
};

3.2. unhandledrejectionによる非同期エラーの監視

Promiseチェーンの最後に.catch()がなかったために発生した未捕捉のPromise失敗(非同期エラー)は、window.onunhandledrejectionイベントで監視します。

JavaScript

window.addEventListener('unhandledrejection', (event) => {
  // event.reason に Promiseの失敗理由(Errorオブジェクトなど)が含まれる
  console.warn("【グローバル監視: 未処理Promise失敗】", event.reason);

  // サーバーへ報告する処理
  reportErrorToServer(event.reason);
  
  // デフォルトのブラウザ警告を防ぐ
  event.preventDefault(); 
});

この二つのグローバルハンドラをセットで実装することが、アプリケーションのエラーカバレッジ(網羅率)を最大化する最も重要な戦略です。

最後に

エラーハンドリングは、単なるバグ潰しではなく、アプリケーションの堅牢性を設計するプロセスです。

  1. 原則:すべての非同期操作(Promise、async関数)には、必ず .catch() または try...catch の処理を組み込みましょう。
  2. 伝播:エラーを処理した後、呼び出し元に問題を知らせる必要がある場合は、必ずthrow errorでエラーを再スロー(再伝播)しましょう。
  3. グローバル監視:window.onerrorunhandledrejection を利用して、開発者の予測を超えて発生した未捕捉エラーを検知し、改善に役立てましょう。

これらの戦略を身につけることで、あなたのアプリケーションは予期せぬ中断から解放され、ユーザーに信頼される高品質なものになるでしょう。

以上、非同期時代のモダンエラーハンドリング戦略についてのご紹介でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。