JavaScript Unhandled Rejection (Promise error) の原因と解決方法【非同期処理の落とし穴と実践的な対処法】

Uncaught (in promise) [ErrorType]: [ErrorMessage] とは

ウェブアプリケーション開発で非同期処理は不可欠ですが、そのエラーハンドリングは時に複雑です。「Uncaught (in promise)」エラーは、Promiseが拒否(rejected)されたにも関わらず、どこにも捕捉されずに実行環境に伝播してしまった場合に発生します。このエラーが表示されると、一見どこで問題が起きているのか分かりづらく、デバッグに時間を要することが少なくありません。

このエラーの核心は、Promiseの拒否が適切に処理されていないことにあります。Promiseが拒否された場合、.catch() メソッド を使うか、async/await と組み合わせた try-catch ブロック で明示的にエラーを捕捉する必要があります。これを怠ると、Promiseは「未処理の拒否(Unhandled Rejection)」として扱われ、このエラーが発生します。

エラーの発生パターン

このエラーは主に以下のようなケースで発生します。

パターン1: Promise.reject() を使ったが .catch() がない

```javascript
function fetchData() {
  return new Promise((resolve, reject) => {
    // 何らかの条件でエラーを発生させる
    if (Math.random() > 0.5) {
      reject(new Error("データ取得に失敗しました"));
    } else {
      resolve("データ取得成功!");
    }
  });
}

// fetchDataはエラーを発生させる可能性があるが、.catch()がない
fetchData().then(data => console.log(data));
// エラーが発生すると "Uncaught (in promise) Error: データ取得に失敗しました" が出力される
```

このパターンでは、`fetchData` 関数内でPromiseが 明示的に `reject()` されていますが、そのPromiseを呼び出している箇所でエラーを捕捉するための {marker}`.catch()` メソッドが記述されていません。Promiseが拒否された場合、そのエラーはどこかで捕捉される必要があります。

```javascript
function fetchData() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) {
      reject(new Error("データ取得に失敗しました"));
    } else {
      resolve("データ取得成功!");
    }
  });
}

fetchData()
  .then(data => console.log(data))
  .catch(error => { // .catch() を追加してエラーを捕捉
    console.error("エラーが発生しました:", error.message);
  });
// エラーが発生しても .catch() で捕捉されるため、Unhandled Rejectionにはならない
```

パターン2: async/await で try-catch ブロックがない

```javascript
async function getUserData() {
  // 存在しないAPIエンドポイントへのフェッチを想定
  const response = await fetch("https://api.example.com/nonexistent-users");
  // HTTPエラーレスポンスの場合、.okがfalseになるが、Promise自体はrejectされない。
  // .json()呼び出しはレスポンスがJSONでない場合にTypeErrorを発生させる可能性がある
  const data = await response.json();
  return data;
}

async function displayUserData() {
  // try-catch ブロックがないため、getUserData内でエラーが発生すると Unhandled Rejection になる
  const userData = await getUserData();
  console.log("ユーザーデータ:", userData);
}

displayUserData();
// 例: TypeError: NetworkError when attempting to fetch resource. (ブラウザ)
// 例: FetchError: invalid json response body at ... (Node.js)
```

`async/await` はPromiseをより同期的なコードのように書ける糖衣構文ですが、内部的にはPromiseを扱っています。`await` 式で待機しているPromiseが拒否された場合、または`await`で待機している関数内で同期的なエラーが発生した場合、その呼び出し元で `try-catch` ブロックを使ってエラーを捕捉する必要があります。上記の例では `fetch` がネットワークエラーを起こしたり、`response.json()` がパースエラーを起こす可能性がありますが、`try-catch` がないためエラーが捕捉されません。

```javascript
async function getUserData() {
  try {
    const response = await fetch("https://api.example.com/nonexistent-users");
    // fetchはネットワークエラー以外ではPromiseをrejectしないため、
    // HTTPステータスコードをチェックして明示的にエラーを投げるのがベストプラクティス
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("ユーザーデータ取得エラー:", error.message);
    // エラーを再スローするか、適切なフォールバック処理を行う
    throw error;
  }
}

async function displayUserData() {
  try { // try-catch ブロックで非同期処理全体を囲む
    const userData = await getUserData();
    console.log("ユーザーデータ:", userData);
  } catch (error) {
    console.error("データ表示エラー:", error.message);
  }
}

displayUserData();
```

パターン3: イベントリスナー内で非同期処理を実行し、エラーが捕捉されない

```javascript
document.getElementById("myButton").addEventListener("click", async () => {
  // このasync関数内で発生したエラーは、イベントリスナーのコールバックの
  // 戻り値としてPromiseを返すため、Uncaught (in promise) になりやすい
  const response = await fetch("https://api.example.com/some-resource");
  // response.json()が失敗する可能性
  const data = await response.json();
  console.log(data);
});

// HTML (body内など): 
```

イベントリスナーのコールバック関数を `async` にした場合、その関数はPromiseを返します。このPromiseが拒否された場合、{marker}イベントリスナーの仕組み上、自動的にエラーが捕捉されるわけではありません。そのため、`async` イベントリスナー内での非同期処理には必ず `try-catch` ブロックを設けるか、`then().catch()` チェーンでエラーを捕捉する必要があります。

```javascript
document.getElementById("myButton").addEventListener("click", async () => {
  try { // try-catch ブロックで囲む
    const response = await fetch("https://api.example.com/some-resource");
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("ボタンクリック時のエラー:", error.message);
    // ユーザーにエラーメッセージを表示するなど、適切なUIフィードバックを行う
  }
});

// HTML (body内など): 
```
Promiseチェーンの途中でエラーを適切に処理しないと、後続の処理が意図せず中断されるだけでなく、アプリケーション全体の動作が不安定になる可能性があります。特に、ユーザーインタラクションに関わる非同期処理では、エラー発生時に適切なフィードバックを返すことが、ユーザー体験の向上に直結します。

根本原因の特定方法

ブラウザのデベロッパーツールの`Console`タブを確認し、`Uncaught (in promise)`メッセージの直後に表示されるスタックトレースを追うのが最も効果的です。エラーの原因となったPromiseが生成された場所や、エラーが最初に発生した非同期処理の箇所を特定しましょう。Node.js環境では、`UnhandledPromiseRejectionWarning`の詳細メッセージとスタックトレースを読み解くことが重要です。

```javascript
// グローバルなエラーリスナーで詳細情報をログ出力
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled Promise Rejection Detected:', event.promise);
  console.error('Reason:', event.reason);
  // event.reason (Errorオブジェクト) のスタックトレースを確認
  if (event.reason && event.reason.stack) {
    console.error('Stack Trace:', event.reason.stack);
  }
  // デフォルトのコンソールエラーを抑制したい場合は event.preventDefault()
  // event.preventDefault();
});

// デバッグ例: Promiseチェーンの各ステップでログ出力
fetch('/api/data')
  .then(response => {
    console.log('Step 1: Response received');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then(data => {
    console.log('Step 2: Data parsed');
    // ここで意図的にエラーを発生させる場合
    // throw new Error("Processing error!");
    console.log(data);
  })
  .catch(error => {
    console.error('Step 3: Error caught:', error.message);
  });
```

防止策とベストプラクティス

このエラーの予防策は、{marker}すべてのPromiseの拒否を明示的に捕捉すること{/marker}に尽きます。Promiseチェーンの最後には必ず`.catch()`メソッドを追加し、`async/await`を使用する場合は関連する非同期処理を`try-catch`ブロックで囲む習慣をつけましょう。これにより、エラーが未処理のまま残ることを防げます。

```javascript
// Promiseチェーンの例
fetch('/api/users')
  .then(response => response.json())
  .then(users => console.log('Users:', users))
  .catch(error => { // 必ず .catch() を追加
    console.error('ユーザー取得エラー:', error);
    // エラーに基づいたUI更新やログ記録
  });

// async/await の例
async function loadUserData() {
  try { // 必ず try-catch で囲む
    const response = await fetch('/api/user/profile');
    if (!response.ok) {
      throw new Error('プロファイル取得失敗');
    }
    const profile = await response.json();
    console.log('Profile:', profile);
  } catch (error) {
    console.error('プロファイルロードエラー:', error);
    // エラーに基づいたUI更新やログ記録
  }
}
loadUserData();

// グローバルなフォールバックとしてのイベントリスナー(開発時や最終手段として)
window.addEventListener('unhandledrejection', event => {
  console.warn('グローバルなUnhandled Rejectionを捕捉しました。個別のエラーハンドリングを見直してください。', event.reason);
  // エラーレポートツールへの送信など
});
```
グローバルな`unhandledrejection`イベントリスナーは、開発中に見落とされたPromiseの拒否を検出するのに役立ちますが、あくまで最後の砦です。理想的には、各非同期処理の呼び出し元で個別にエラーを捕捉・処理することで、より堅牢で予測可能なアプリケーションを構築できます。

よくある質問(FAQ)

Q
本番環境でだけ「Uncaught (in promise)」が発生するケースはありますか?
A

はい、よくあります。本番環境ではネットワーク状況が不安定であったり、開発環境とは異なるCORSポリシー、外部APIのレート制限やダウンタイムなど、特定の条件が重なってエラーが発生しやすくなります。開発環境では問題なくても、本番環境のログ監視は非常に重要です。

Q
ReactやVueなどのフレームワークで「Uncaught (in promise)」が発生した場合の典型的なパターンと対処法は何ですか?
A

Reactの`useEffect`やVueの`mounted`フック内で`async/await`を使用し、`try-catch`で囲み忘れているケースが多いです。また、コンポーネントがアンマウントされた後に非同期処理の結果で状態を更新しようとして発生することもあります。これには`isMounted`フラグやクリーンアップ関数で対処します。

Q
Linterや開発ツールを使って「Uncaught (in promise)」を事前に防止する方法はありますか?
A

ESLintの`eslint-plugin-promise`プラグインにある`no-floating-promises`ルールが非常に有効です。これにより、`Promise`が返されるが`catch`で処理されていない箇所を警告してくれます。TypeScriptを使用している場合は、コンパイラが型安全性をチェックするのに役立ちます。

Q
エラー発生時、ユーザーにどのようなエラーハンドリングを行うべきですか?
A

ユーザーには、何が起こったのかを明確に伝え、可能であれば解決策(「しばらくしてから再試行してください」など)を提示するのが理想です。例えば、エラーメッセージをUI上に表示したり、リトライボタンを提供したり、機能のフォールバック処理を行うなど、ユーザー体験を損なわないよう配慮することが重要です。

Q
`Promise.all`や`Promise.race`で`Unhandled Rejection`を防ぐにはどうすればよいですか?
A

`Promise.all`や`Promise.race`に渡す個々のPromiseも、それぞれ適切に`.catch()`でエラーハンドリングしておくのが基本です。`Promise.all`はどれか一つでも`reject`されると全体が`reject`されるため、`Promise.allSettled`を使うことで、全てのPromiseの結果(成功/失敗問わず)を取得し、個別に処理することも検討できます。

Q
`window.addEventListener(‘unhandledrejection’, …)` を使う際の注意点はありますか?
A

このイベントリスナーは、あくまで「未処理の拒否」をグローバルに捕捉するための最終手段であり、個々のPromiseでエラーを処理する代替手段ではありません。開発中に見落としがないかを確認したり、エラーレポートツールに送信するために使うのが適切です。プロダクションでこれを頼りすぎると、どこでエラーが発生したかの特定が難しくなることがあります。

Q
`async/await`を使っているのに「Uncaught (in promise)」が発生するのはなぜですか?
A

`async/await`はPromiseの糖衣構文であり、`await`で待機しているPromiseが拒否された場合や、`async`関数内で同期的なエラーが発生した場合、その`async`関数自体が拒否されたPromiseを返します。この返されたPromiseが`try-catch`ブロックや`.catch()`メソッドで捕捉されないと、「Unhandled Rejection」となります。`await`の呼び出し元で`try-catch`を忘れていないか確認しましょう。

この用語と一緒に知っておきたい用語

用語 この記事との関連
NULL Promiseが解決または拒否される際に、予期せず`null`値が渡されると後続処理でエラーを引き起こす可能性があるため。
イベントドリブン JavaScriptの非同期処理やUIイベントはイベントドリブンな特性を持ち、Promiseエラーもイベントとして扱われるため。
デバッガ `Unhandled Rejection`のような非同期エラーの原因特定には、ブラウザのデバッガやNode.jsのデバッグツールが不可欠であるため。
DRY原則 エラーハンドリングの共通化や重複排除において、`DRY`原則を意識することでコードの品質を保つことができるため。
スループット 非同期処理のエラーが適切に処理されないと、アプリケーションのスループットやパフォーマンスに悪影響を与える可能性があるため。
免責事項: 当記事の情報は執筆時点の内容に基づいています。最新情報は各公式サイトをご確認ください。当サイトは情報提供を目的としており、資格取得・技術的対応の結果について一切の責任を負いません。

コメント