---
title: "JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け"
url: "https://www.issoh.co.jp/tech/details/3691/"
published: 2024-10-01
updated: 2026-08-04
categories: ["JavaScript"]
publisher: "株式会社一創"
---

# JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け

非同期処理の事故は、書き方ではなく制御の側で起きます。エラーが握りつぶされて成功扱いで進む、10件のAPI呼び出しを直列で回して10倍の時間がかかる、1件の失敗で全体が落ちる。ここでは例外の伝播、`fetch` の失敗判定、`Promise.all` と `allSettled` の選び分け、タイムアウトとリトライまでを実装コードで扱います。`async`/`await` の文法とTypeScriptの型付けは[非同期関数の書き方｜async宣言・await式・TypeScriptの型定義とdelay関数の実装](/tech/details/3794/)に、非同期処理そのものの考え方は[非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)にまとめてあります。

## まとめ｜非同期のエラー処理と並列実行で押さえる要点

- `await` を付け忘れたPromiseのrejectは `try/catch` では捕まりません。Node.js 15以降は既定でプロセスが落ちます。
- `fetch` は404や500では例外になりません。`res.ok` を見て自分で分岐しない限り、エラーページのHTMLを正常な応答として処理してしまいます。
- 全件成功が前提なら `Promise.all`、1件失敗しても残りの結果が要るなら `Promise.allSettled` です。
- `for` ループの中の `await` は直列実行です。所要時間は各処理の合計になります。互いに依存しない処理は `map` して `Promise.all` に渡します。
- タイムアウトは `AbortSignal.timeout(ms)` を `fetch` の `signal` に渡すのが最短です（Node.js 17.3.0以降、Chrome 124以降）。

## try/catchで捕まる例外と、捕まらない例外

### awaitの付け忘れで素通りするreject

`try/catch` が捕まえるのは、その `try` ブロックの実行中に投げられた例外だけです。`await` を書かずに呼んだ非同期関数は、Promiseを返した時点でブロックを抜けています。中でrejectされても、catch節には届きません。

```
try {
  save(order);          // await を書き忘れている
} catch (e) {
  console.error(e);     // ここには来ない。処理は成功したように見える
}

try {
  await save(order);    // await があって初めて catch に伝わる
} catch (e) {
  console.error(e);
}
```

行き場を失ったrejectは未処理拒否（unhandled rejection）になります。Node.jsは15で既定の挙動を警告からthrowへ変更したため、ハンドラを登録していないプロセスはそこで終了します。ブラウザでは `window` の `unhandledrejection` イベントに流れ、監視ツールを入れていなければコンソールに残るだけです。「例外は出ていないのにデータが保存されていない」という報告の多くはこの形です。

### 再throwとcauseによる原因の保持

catchした例外を握りつぶすと、上位の層は何が起きたか分からなくなります。文脈を足して投げ直すときは、元の例外を `cause` に入れます。

```
async function loadOrder(id) {
  try {
    return await api.get(`/orders/${id}`);
  } catch (e) {
    // 元の例外を cause に保持したまま、業務上の文脈を足して投げ直す
    throw new Error(`注文の取得に失敗しました: ${id}`, { cause: e });
  } finally {
    metrics.record('order.load');   // 成否にかかわらず必ず実行される
  }
}
```

`cause` はES2022で入った標準の引数で、スタックトレースを追うときに元の失敗（接続断なのか404なのか）を残せます。`finally` は `return` の後でも実行されるため、接続の解放や計測はここに置きます。ログを出すだけで再throwしない `catch` は、呼び出し側から見れば成功なので書かないでください。

## fetchのエラー判定｜4xx・5xxでは例外にならない

`fetch` がrejectするのは、通信そのものが成立しなかったときだけです。DNS解決の失敗、接続断、CORS違反、中断がこれにあたります。サーバーが404や500を返した場合、通信は成功しているのでPromiseは解決します。

```
const res = await fetch('/api/orders');

if (!res.ok) {
  // res.ok は status が 200〜299 のときだけ true
  throw new Error(`APIエラー: ${res.status} ${res.statusText}`);
}

const data = await res.json();   // ここも失敗しうる（HTMLが返れば構文エラー）
```

`res.ok` を見ずに `res.json()` を呼ぶと、エラーページのHTMLをJSONとして解析しようとして構文エラーになります。この例外は `fetch` の行ではなく `json()` の行で出るため、原因がAPI側のエラー応答だと気付くまで時間を取られます。`fetch` を直接書く代わりにHTTPクライアントを挟む設計が好まれるのは、この判定を1箇所に閉じ込められるからです。

## 並列と順次の使い分け｜Promise.all・allSettled・any・race

### 4つの合成メソッドの違い

複数のPromiseをまとめる関数は4つあり、失敗したときの振る舞いで選びます。

| メソッド               | 解決する条件 | 失敗時                    | 主な用途           |
| ------------------ | ------ | ---------------------- | -------------- |
| Promise.all        | 全件成功   | 1件でも失敗した時点でreject      | 全部揃わないと画面を出せない |
| Promise.allSettled | 全件決着   | rejectしない（status付きで返る） | 一括処理の成否レポート    |
| Promise.any        | 最初の成功  | 全滅時にAggregateError     | 複数ミラーからの取得     |
| Promise.race       | 最初の決着  | 最初が失敗ならreject          | タイムアウトとの競走     |

```
// 全件必要：1件でも失敗すれば例外
const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)]);

// 失敗込みで全件の結果が欲しい
const results = await Promise.allSettled(ids.map((i) => save(i)));
const failed = results.filter((r) => r.status === 'rejected');
console.log(`${failed.length}件失敗`, failed.map((r) => r.reason));
```

`Promise.all` が1件の失敗でrejectしても、残りの処理が中断されるわけではありません。走り続けて、結果だけが捨てられます。副作用のある処理（課金、メール送信）を `all` で束ねると、「例外は出たが半分は実行済み」という状態が生まれます。この用途では `allSettled` で全件の成否を受け取り、失敗分だけを補償する形にしてください。`allSettled` はNode.js 12.9.0・Chrome 76以降、`any` はNode.js 15.0.0・Chrome 85以降で使えます。

### forループ内のawaitが直列になる理由

ループの中で `await` を書くと、前の処理が終わるまで次が始まりません。1件200msのAPIを10件呼べば2秒かかります。

```
// 直列：合計2秒（依存関係がある場合はこれが正しい）
for (const id of ids) {
  await fetchDetail(id);
}

// 並列：最も遅い1件の時間で終わる
await Promise.all(ids.map((id) => fetchDetail(id)));
```

逆に、前の結果を次のリクエストで使う場合や、更新の順序が結果に影響する場合は直列が正解です。「遅いから全部並列に」ではなく、依存関係の有無で決めます。`map` の中に `async` コールバックを書いた時点で全件が同時に走り始める点にも注意してください。`Promise.all` に渡す前から通信は始まっています。

### 同時実行数の制御

1,000件の配列をそのまま `Promise.all` に渡すと、1,000本の接続を同時に開こうとします。相手のレート制限に当たるか、自分のメモリを使い切ります。

```
async function mapLimit(items, limit, worker) {
  const results = [];
  let cursor = 0;
  const runners = Array.from({ length: limit }, async () => {
    while (cursor < items.length) {
      const index = cursor++;              // 各ランナーが順に次の要素を取る
      results[index] = await worker(items[index]);
    }
  });
  await Promise.all(runners);
  return results;
}

const details = await mapLimit(ids, 5, fetchDetail);   // 同時5本まで
```

並列数は相手の制限から逆算します。APIの上限が毎秒10リクエストなら、平均応答時間0.5秒として同時5本が目安です。上限が公開されていない外部APIでは、まず3〜5本から始めて429の有無を見ます。

## タイムアウトと中断｜AbortSignal.timeoutとAbortController

応答が返らない相手を待ち続けないために、`fetch` にはタイムアウトを設定します。`AbortSignal.timeout(ms)` は指定時間で自動的に中断するシグナルを作ります。

```
// 3秒で打ち切る
try {
  const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
} catch (e) {
  if (e.name === 'TimeoutError') console.error('3秒以内に応答がありません');
  if (e.name === 'AbortError') console.error('中断されました');
}

// 画面遷移などで能動的に打ち切る場合は AbortController
const ac = new AbortController();
document.querySelector('#cancel').addEventListener('click', () => ac.abort());
const res = await fetch(url, { signal: ac.signal });
```

`AbortSignal.timeout()` はNode.js 17.3.0、Firefox 100、Safari 16で使えます。Chromeは103で入りましたが、123までは中断時に `TimeoutError` ではなく `AbortError` を投げる部分実装で、名前で分岐するコードは124以降が前提です。ユーザー操作による中断とタイムアウトを1つのシグナルにまとめたい場合は `AbortSignal.any([...])`（Node.js 20.3.0・Chrome 116以降）を使います。

`Promise.race` でタイマーと競走させる古い書き方は、負けた側の `fetch` が裏で走り続ける点が違います。接続を実際に切りたいなら `signal` を使ってください。

## リトライしてよい失敗と、してはいけない失敗の線引き

再試行は万能ではありません。**同じリクエストを2回実行しても結果が変わらない（冪等である）操作にだけ入れます。** 決済や送信のように、2回実行されると二重課金・二重送信になる操作は、リトライの前に冪等キーの仕組みを用意するのが先です。

```
async function retry(fn, { attempts = 3, base = 500 } = {}) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      const retriable = e.status === 429 || e.status >= 500 || e.name === 'TimeoutError';
      if (!retriable || i === attempts - 1) throw e;   // 400番台は再試行しない
      await new Promise((r) => setTimeout(r, base * 2 ** i));
    }
  }
}
```

再試行してよいのは、429（レート制限）、5xx（サーバー側の一時障害）、タイムアウトと接続断です。400や422のような入力の誤りは、何度送っても同じ結果にしかならず、相手の負荷を増やすだけです。401も、トークンを更新せずに繰り返せばアカウントロックを招きます。

待ち時間は固定にせず、失敗のたびに倍にします（指数バックオフ）。障害から復旧した瞬間に全クライアントが同時に殺到する事態を避けるには、さらに乱数のゆらぎを足します。冪等性そのものの設計は[データパイプラインとは？種類・構成要素と冪等性から考える設計判断を解説【2026年版】](/tech/details/15848/)で扱っています。

## よくある質問

### JavaScriptの非同期処理でエラーが捕まらないのはなぜですか？

`await` の付け忘れが最も多い原因です。`try` ブロックの中で非同期関数を `await` なしで呼ぶと、Promiseが返った時点でブロックを抜けるため、後からrejectされてもcatchには届きません。`.catch()` も付けていない場合は未処理拒否となり、Node.js 15以降は既定でプロセスが終了します。

### 非同期処理と並列処理の違いは何ですか？

非同期処理は「完了を待たずに次へ進む」実行方式、並列処理は「複数の処理を同時に走らせる」実行形態です。JavaScriptは単一スレッドで動くため、`Promise.all` で同時に走らせているのはI/Oの待ち時間であって、計算そのものが並列になるわけではありません。CPUを使う処理を速くしたい場合はWeb Workerなど別スレッドの仕組みが要ります。

### await fetchで404を検出するにはどうすればよいですか？

`res.ok`（`status` が200〜299のときのみtrue）を確認して自分で例外を投げます。`fetch` はネットワーク層の失敗だけをrejectするため、404や500はそのまま解決されます。ステータスコードで分岐したい場合は `res.status` を直接見ます。

### 非同期処理は英語で何と言いますか？

asynchronous processing、実行単位を指す場合は asynchronous operation です。同期処理は synchronous processing で、コード上の `sync`／`async` はこの語の略です。ライブラリのドキュメントでは「non-blocking（ブロックしない）」という表現も同じ性質を指して使われます。

### 複数のAPIを呼ぶとき、どれを使えば速くなりますか？

互いに依存しない呼び出しなら `Promise.all` か `Promise.allSettled` に渡します。所要時間は合計から「最も遅い1件」に変わります。ただし件数が多い場合は同時実行数を絞ってください。相手のレート制限に当たると429が返り、リトライで余計に遅くなります。

## 関連記事

- [非同期関数の書き方｜async宣言・await式・TypeScriptの型定義とdelay関数の実装](/tech/details/3794/)
- [非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)
- [anyio・Trio・asyncioの違いと使い分け｜Python非同期ライブラリ比較](/tech/details/4346/)
- [Node.js 26の新機能・変更点とLTSスケジュール総まとめ](/tech/details/12014/)
- [データパイプラインとは？種類・構成要素と冪等性から考える設計判断を解説【2026年版】](/tech/details/15848/)

---

出典: [JavaScriptの非同期エラーハンドリングと並列実行｜try/catch・Promise.all・allSettledの使い分け](<https://www.issoh.co.jp/tech/details/3691/>)（株式会社一創）
