async/await完全実践ガイド〜並列・直列・エラー処理・キャンセル・Top-level await・React 19対応【2026年版】〜

async/await は 2017 年に ECMAScript 2017 で標準化されて以来、JavaScript の非同期処理を書く事実上の標準構文となりました。とはいえ、現場では「Promise.all との使い分けが分からない」「forEach で await が効かない」「キャンセル処理が分からない」「Top-level await って使っていいの?」といった疑問が後を絶ちません。

本記事では ECMAScript 2025 / Node.js 22+ / TypeScript 5.x 準拠で、コピペで動く 40 以上のコードサンプルを通して、基本構文 → 並列・直列パターン → エラー処理 → キャンセル(AbortController) → タイムアウト → リトライ → 並列数制限 → Async Iterator → Top-level await → React 19 / Express / Node.js 連携まで、async/await 単体としての実践知見を完全網羅します。

本記事は「非同期処理パターン」観点に特化しているため、JavaScript ベストプラクティス 10 選(言語全般の観点)、および useEffect 完全ガイド(React 副作用フックの観点)と相互補完的に読むと、フロントエンドからバックエンドまでの非同期処理を一気通貫で押さえることができます。

  1. 1. 非同期処理の基礎 – Promise 復習
    1. 1.1 Promise の状態遷移
    2. 1.2 then / catch / finally の連鎖
    3. 1.3 Promise を返す関数を書く
  2. 2. async / await の基本構文
    1. 2.1 async 関数とは「常に Promise を返す関数」
    2. 2.2 await は「Promise の解決を待つ」
    3. 2.3 await を付け忘れた場合の挙動
  3. 3. Promise.then から async/await へのリファクタ
    1. 3.1 直列処理(連続 fetch)
    2. 3.2 条件分岐を含む処理
    3. 3.3 ループを含む処理
  4. 4. 並列処理 – Promise.all / race / allSettled / any
    1. 4.1 Promise.all – 全件成功で結果配列を返す
    2. 4.2 Promise.allSettled – 失敗込みで全件結果を取得
    3. 4.3 Promise.race – 最初の確定を採用
    4. 4.4 Promise.any – 最初の成功を採用
  5. 5. 直列 vs 並列の使い分け実例
    1. 5.1 並列が正解のケース(独立処理)
    2. 5.2 直列が正解のケース(順序保証・レート制限)
    3. 5.3 forEach に async は効かない
  6. 6. エラーハンドリング戦略
    1. 6.1 基本の try/catch
    2. 6.2 fetch は HTTP エラーで reject しない
    3. 6.3 カスタムエラークラスで型分岐
    4. 6.4 unhandledrejection の捕捉
  7. 7. キャンセル処理 – AbortController
    1. 7.1 fetch のキャンセル
    2. 7.2 タイムアウトを AbortSignal.timeout で実装
    3. 7.3 AbortSignal.any で複数シグナルを束ねる
    4. 7.4 React useEffect でのキャンセル
  8. 8. タイムアウト・リトライ・指数バックオフ
    1. 8.1 シンプルな retry 関数
    2. 8.2 指数バックオフ + Jitter
    3. 8.3 リトライ対象を限定する
  9. 9. 並列数制限 – p-limit パターン
    1. 9.1 p-limit ライブラリの使用例
    2. 9.2 自作の並列制限関数
  10. 10. Async Iterator と for await…of
    1. 10.1 Async Generator でページネーション
    2. 10.2 Stream を Async Iterator として読む
    3. 10.3 OpenAI のような SSE ストリーミング
  11. 11. Top-level await(ESM)
    1. 11.1 IIFE が不要に
    2. 11.2 動的 import との組み合わせ
    3. 11.3 Top-level await の注意点
  12. 12. Node.js での async/await 実例
    1. 12.1 fs/promises – ファイル操作
    2. 12.2 子プロセスを Promise で扱う
    3. 12.3 Express + async/await
  13. 13. React と async/await
    1. 13.1 useEffect 内の async は IIFE で囲う
    2. 13.2 イベントハンドラは async OK
    3. 13.3 React 19 の use() フック
    4. 13.4 Next.js Server Components – async コンポーネント
  14. 14. TypeScript と async/await
    1. 14.1 戻り値型を明示する
    2. 14.2 Promise.all のタプル推論
    3. 14.3 Result 型でエラーを値として扱う
  15. 15. アンチパターン総まとめ
    1. 15.1 不要な return await
    2. 15.2 並列にできるのに直列で書く
    3. 15.3 Promise コンストラクタアンチパターン
    4. 15.4 async を付けただけで await を使わない
  16. 16. パフォーマンス最適化のポイント
    1. 16.1 ループの外で await をまとめる
    2. 16.2 マイクロタスクとイベントループ
  17. 17. テストでの async/await
  18. 18. async/await 学習のロードマップ
  19. 19. よくある質問(FAQ)
    1. Q. async/await と Promise.then はどちらを使うべき?
    2. Q. await を付けないとどうなる?
    3. Q. forEach の中で await できないのはなぜ?
    4. Q. Promise.all と Promise.allSettled の使い分けは?
    5. Q. Top-level await は本番で使って大丈夫?
    6. Q. キャンセルした fetch はサーバー側で止まる?
    7. Q. async/await と Web Worker はどう関係する?
  20. 20. まとめ – async/await を制する者は JavaScript を制する

1. 非同期処理の基礎 – Promise 復習

async/await は Promise のシンタックスシュガーです。内部実装を理解するために、まず Promise の基本 3 メソッド then / catch / finally から復習します。

1.1 Promise の状態遷移

Promise は pending(未解決)→ fulfilled(成功)または rejected(失敗)に必ず一度だけ遷移します。一度確定した状態は二度と変わりません。

// Promise の最小実装
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve("成功!");
    } else {
      reject(new Error("失敗..."));
    }
  }, 1000);
});

promise
  .then((value) => console.log("then:", value))
  .catch((err) => console.error("catch:", err.message))
  .finally(() => console.log("finally: 完了"));

1.2 then / catch / finally の連鎖

then は値の変換、catch はエラー処理、finally はクリーンアップに使い分けます。then の中で return した値は次の then に渡されるのがポイントです。

fetch("/api/users/1")
  .then((res) => res.json())            // Response → JSON へ変換
  .then((user) => user.name.toUpperCase()) // user → name に変換
  .then((name) => console.log(name))    // 最終結果を表示
  .catch((err) => console.error(err))   // 途中のどこで失敗してもここで捕捉
  .finally(() => console.log("完了"));  // 成否に関わらず実行

1.3 Promise を返す関数を書く

非同期 API をラップするユーティリティは Promise を返す関数として書きます。下記は setTimeout を Promise 化した古典的な sleep 関数です。

// setTimeout を Promise 化
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// 使用例
sleep(1000).then(() => console.log("1 秒経過"));

// async/await で書くと
(async () => {
  console.log("待ちます...");
  await sleep(2000);
  console.log("2 秒経過");
})();

2. async / await の基本構文

async/await は Promise を「同期的に見える書き方」で扱える構文です。学習コストは低いものの、いくつかの落とし穴があります。

2.1 async 関数とは「常に Promise を返す関数」

関数の頭に async を付けると、その関数の戻り値は必ず Promise でラップされます。中で return した値も Promise.resolve でラップされます。

// 普通の数値を返しているように見えるが...
async function getNumber() {
  return 42;
}

// 実際は Promise<number> が返る
const result = getNumber();
console.log(result); // Promise { 42 }
result.then((v) => console.log(v)); // 42

// 例外を throw すると rejected な Promise になる
async function getError() {
  throw new Error("失敗");
}
getError().catch((e) => console.error(e.message)); // "失敗"

2.2 await は「Promise の解決を待つ」

awaitasync 関数の中(または Top-level)でのみ使えます。await の右辺が Promise なら解決を待ち、Promise でない値ならそのまま値として扱われます。

async function fetchUser(id) {
  // Response を待つ
  const res = await fetch(`/api/users/${id}`);
  // JSON パースを待つ
  const user = await res.json();
  return user;
}

// 戻り値も Promise なので、呼び出し側でも await が必要
async function main() {
  const user = await fetchUser(1);
  console.log(user.name);
}
main();

2.3 await を付け忘れた場合の挙動

await を付け忘れると、Promise オブジェクトそのものが変数に入ります。.name プロパティは undefined になり、原因不明のバグになります。ESLint の no-floating-promises ルールで検出可能です。

// ❌ await 忘れ
async function ng() {
  const user = fetchUser(1); // ← await なし
  console.log(user.name);    // undefined(user は Promise)
}

// ✅ 正しい
async function ok() {
  const user = await fetchUser(1);
  console.log(user.name);    // "Alice"
}

3. Promise.then から async/await へのリファクタ

レガシーコードを async/await に書き換える Before/After を多数提示します。基本ルールは「then の戻り値を const で受ける」です。

3.1 直列処理(連続 fetch)

// ❌ Before: Promise チェーン
function getUserPosts(id) {
  return fetch(`/api/users/${id}`)
    .then((res) => res.json())
    .then((user) => fetch(`/api/posts?userId=${user.id}`))
    .then((res) => res.json())
    .catch((err) => {
      console.error(err);
      return [];
    });
}

// ✅ After: async/await
async function getUserPosts(id) {
  try {
    const userRes = await fetch(`/api/users/${id}`);
    const user = await userRes.json();
    const postsRes = await fetch(`/api/posts?userId=${user.id}`);
    return await postsRes.json();
  } catch (err) {
    console.error(err);
    return [];
  }
}

3.2 条件分岐を含む処理

条件分岐が絡むと then チェーンは急速に読みにくくなります。async/await なら通常の if/else がそのまま使えます。

// ❌ Before
function getProfile(id) {
  return fetchUser(id).then((user) => {
    if (user.isAdmin) {
      return fetchAdminProfile(user.id).then((p) => ({ ...user, ...p }));
    } else {
      return fetchNormalProfile(user.id).then((p) => ({ ...user, ...p }));
    }
  });
}

// ✅ After
async function getProfile(id) {
  const user = await fetchUser(id);
  const profile = user.isAdmin
    ? await fetchAdminProfile(user.id)
    : await fetchNormalProfile(user.id);
  return { ...user, ...profile };
}

3.3 ループを含む処理

then チェーンで複数件を順番に処理するには reduce を使う必要があり可読性が悪いです。async/await なら for...of で素直に書けます。

// ❌ Before: reduce で直列化
function processIds(ids) {
  return ids.reduce(
    (promise, id) =>
      promise.then((acc) => fetchOne(id).then((r) => [...acc, r])),
    Promise.resolve([])
  );
}

// ✅ After: for...of
async function processIds(ids) {
  const results = [];
  for (const id of ids) {
    results.push(await fetchOne(id));
  }
  return results;
}

4. 並列処理 – Promise.all / race / allSettled / any

独立した複数の非同期処理を行う場合、直列で待つのは時間の無駄です。Promise の 4 つの静的メソッドを使い分けます。

4.1 Promise.all – 全件成功で結果配列を返す

最もよく使うパターン。1 件でも失敗すると全体が即座に reject されます。「全件揃わないと意味がない」処理に使います。

// 3 つの API を並列に叩く
async function loadDashboard(userId) {
  const [user, posts, notifications] = await Promise.all([
    fetch(`/api/users/${userId}`).then((r) => r.json()),
    fetch(`/api/posts?userId=${userId}`).then((r) => r.json()),
    fetch(`/api/notifications?userId=${userId}`).then((r) => r.json()),
  ]);
  return { user, posts, notifications };
}

// 直列で書くと約 3 倍時間がかかる
async function slowVersion(userId) {
  const user = await fetch(`/api/users/${userId}`).then((r) => r.json());
  const posts = await fetch(`/api/posts?userId=${userId}`).then((r) => r.json());
  const notifications = await fetch(`/api/notifications?userId=${userId}`).then((r) => r.json());
  return { user, posts, notifications };
}

4.2 Promise.allSettled – 失敗込みで全件結果を取得

「一部失敗しても残りは欲しい」場合は allSettled を使います。結果配列の各要素は { status: "fulfilled", value } または { status: "rejected", reason } の形式です。

const urls = [
  "/api/posts/1",
  "/api/posts/2",
  "/api/posts/9999", // 404 になる想定
];

const results = await Promise.allSettled(
  urls.map((url) => fetch(url).then((r) => r.json()))
);

for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("OK:", r.value);
  } else {
    console.error("NG:", r.reason);
  }
}

// 成功分だけ取り出す
const success = results
  .filter((r) => r.status === "fulfilled")
  .map((r) => r.value);

4.3 Promise.race – 最初の確定を採用

成功・失敗を問わず最初に確定した Promise の結果が採用されます。タイムアウト実装でよく使われます。

// fetch にタイムアウトを付ける
function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`timeout ${ms}ms`)), ms)
    ),
  ]);
}

try {
  const res = await fetchWithTimeout("/api/slow", 3000);
  console.log(await res.json());
} catch (err) {
  console.error(err.message); // "timeout 3000ms"
}

4.4 Promise.any – 最初の成功を採用

any最初に成功した Promise の値を返します。全件失敗した場合のみ AggregateError を投げます。冗長化されたエンドポイントから「とにかく速く返ってきたもの」が欲しい場合に有効です。

// 3 つのミラー CDN から最速で取得
const mirrors = [
  "https://cdn-tokyo.example.com/data.json",
  "https://cdn-osaka.example.com/data.json",
  "https://cdn-fukuoka.example.com/data.json",
];

try {
  const res = await Promise.any(mirrors.map((url) => fetch(url)));
  const data = await res.json();
  console.log("最速 CDN から取得:", data);
} catch (err) {
  // 全 CDN が落ちている場合のみ AggregateError
  console.error("all failed:", err.errors);
}

5. 直列 vs 並列の使い分け実例

「いつ Promise.all を使い、いつ for…of で順番に処理するか」は実務で頻繁に判断ミスが起こるポイントです。基準は「処理同士に依存関係があるか / サーバー負荷を抑えたいか」です。

5.1 並列が正解のケース(独立処理)

// ✅ 並列 - 各 fetch は互いに独立
async function fetchAll(ids) {
  return Promise.all(ids.map((id) => fetch(`/api/items/${id}`).then((r) => r.json())));
}

5.2 直列が正解のケース(順序保証・レート制限)

// ✅ 直列 - DB トランザクションの順序を維持
async function migrate(records) {
  for (const r of records) {
    await db.insert(r); // 順番に INSERT
  }
}

// ✅ 直列 - 外部 API の Rate Limit を尊重(1 req/sec)
async function syncWithRateLimit(items) {
  for (const item of items) {
    await externalApi.post(item);
    await sleep(1000); // 1 秒待つ
  }
}

5.3 forEach に async は効かない

最頻出のバグです。Array.prototype.forEach は引数の関数の戻り値(Promise)を無視するため、「forEach の中で await しても外側では完了を待ってくれない」という挙動になります。

// ❌ NG - forEach は Promise を待たない
async function ng(ids) {
  ids.forEach(async (id) => {
    await processOne(id); // 待つように見えるが
  });
  console.log("完了"); // ← 実際は全 processOne の前に出る
}

// ✅ 直列にしたい場合は for...of
async function okSerial(ids) {
  for (const id of ids) {
    await processOne(id);
  }
  console.log("完了");
}

// ✅ 並列にしたい場合は Promise.all + map
async function okParallel(ids) {
  await Promise.all(ids.map((id) => processOne(id)));
  console.log("完了");
}

6. エラーハンドリング戦略

async 関数内の例外は同期コードと同じく try/catch で捕捉できます。とはいえ、Promise 特有の落とし穴がいくつかあります。

6.1 基本の try/catch

async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }
    return await res.json();
  } catch (err) {
    console.error("getUser failed:", err);
    throw err; // 上位に再 throw
  }
}

6.2 fetch は HTTP エラーで reject しない

fetch API はネットワーク失敗以外では reject しない仕様です。404 や 500 でも resolved な Response が返ります。必ず res.ok または res.status でチェックしてください。

// ❌ HTTP 500 でも catch に入らない
async function ng(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    return await res.json(); // 500 のエラー JSON が返るかも
  } catch (err) {
    // ネットワーク断・DNS 失敗時のみ
  }
}

// ✅ ステータスを明示的にチェック
async function ok(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`API error ${res.status}: ${text}`);
  }
  return res.json();
}

6.3 カスタムエラークラスで型分岐

エラーの種類によって扱いを変えたい場合はカスタムエラークラスを定義します。TypeScript では instanceof で型 narrowing もできます。

class NotFoundError extends Error {
  constructor(public id: string) {
    super(`Not found: ${id}`);
    this.name = "NotFoundError";
  }
}

class UnauthorizedError extends Error {
  constructor() {
    super("Unauthorized");
    this.name = "UnauthorizedError";
  }
}

async function fetchOrThrow(id: string) {
  const res = await fetch(`/api/items/${id}`);
  if (res.status === 404) throw new NotFoundError(id);
  if (res.status === 401) throw new UnauthorizedError();
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

try {
  await fetchOrThrow("xxx");
} catch (err) {
  if (err instanceof NotFoundError) {
    console.warn("ID なし:", err.id);
  } else if (err instanceof UnauthorizedError) {
    location.href = "/login";
  } else {
    throw err;
  }
}

6.4 unhandledrejection の捕捉

どこかで catch されていない Promise rejection は unhandledrejection イベントで補足できます。本番アプリではエラートラッキングサービス(Sentry など)に送るのが定番です。

// ブラウザ
window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled rejection:", event.reason);
  // Sentry.captureException(event.reason);
  event.preventDefault(); // コンソール警告を抑制
});

// Node.js
process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  process.exit(1); // strict にクラッシュさせる
});

7. キャンセル処理 – AbortController

Promise には標準のキャンセル機構がないため、AbortController + signal で実装します。fetch / setTimeout / Stream など、ほとんどの新しい API は AbortSignal をサポートしています。

7.1 fetch のキャンセル

const controller = new AbortController();

const promise = fetch("/api/heavy", { signal: controller.signal })
  .then((r) => r.json())
  .catch((err) => {
    if (err.name === "AbortError") {
      console.log("リクエストをキャンセルしました");
    } else {
      throw err;
    }
  });

// 5 秒後にキャンセル
setTimeout(() => controller.abort(), 5000);

7.2 タイムアウトを AbortSignal.timeout で実装

2026 年現在、AbortSignal.timeout(ms) が全主要ブラウザと Node.js 18+ で利用可能です。手書きの setTimeout より圧倒的に簡潔です。

// ✅ モダンな書き方
async function fetchWithTimeout(url, ms = 5000) {
  const res = await fetch(url, { signal: AbortSignal.timeout(ms) });
  return res.json();
}

try {
  const data = await fetchWithTimeout("/api/slow", 3000);
} catch (err) {
  if (err.name === "TimeoutError") {
    console.error("タイムアウト");
  }
}

7.3 AbortSignal.any で複数シグナルを束ねる

ES2024 で追加された AbortSignal.any([...signals]) は、複数のシグナルのうちどれかが abort されたら全体をキャンセルする合成シグナルを作ります。「ユーザーキャンセル + タイムアウト」の組み合わせに最適です。

const userAbort = new AbortController();
const combined = AbortSignal.any([
  userAbort.signal,
  AbortSignal.timeout(10_000), // 10 秒
]);

const res = await fetch("/api/data", { signal: combined });

7.4 React useEffect でのキャンセル

useEffect 内でデータ取得する際、コンポーネントのアンマウントや依存変更で古いリクエストをキャンセルするのは必須テクニックです。詳細は useEffect 完全ガイド も参照してください。

useEffect(() => {
  const controller = new AbortController();

  (async () => {
    try {
      const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
      const user = await res.json();
      setUser(user);
    } catch (err) {
      if (err.name !== "AbortError") setError(err);
    }
  })();

  return () => controller.abort(); // cleanup でキャンセル
}, [id]);

8. タイムアウト・リトライ・指数バックオフ

外部 API を叩く実装では、ネットワーク不安定への対処が必須です。リトライ + 指数バックオフ + Jitter の組み合わせがデファクトです。

8.1 シンプルな retry 関数

async function retry<T>(fn: () => Promise<T>, times = 3): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < times; i++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      console.warn(`retry ${i + 1}/${times}`, err);
    }
  }
  throw lastErr;
}

// 使用例
const data = await retry(() => fetch("/api/flaky").then((r) => r.json()), 5);

8.2 指数バックオフ + Jitter

固定間隔のリトライは「同時に再接続が殺到する Thundering Herd 問題」を引き起こします。2 のべき乗で待ち時間を増やす(指数バックオフ)+ ランダム揺らぎ(Jitter)が AWS SDK などの推奨実装です。

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: { maxAttempts?: number; baseMs?: number; maxMs?: number } = {}
): Promise<T> {
  const { maxAttempts = 5, baseMs = 200, maxMs = 10_000 } = options;
  let lastErr: unknown;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      const exp = Math.min(baseMs * 2 ** attempt, maxMs);
      const jitter = Math.random() * exp * 0.5;
      await sleep(exp + jitter);
    }
  }
  throw lastErr;
}

8.3 リトライ対象を限定する

4xx 系(クライアントエラー)はリトライしても意味がありません。5xx / ネットワークエラーのみ再試行するように分岐します。

async function retryableFetch(url: string, attempts = 3): Promise<Response> {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url);
    if (res.ok) return res;
    if (res.status >= 400 && res.status < 500) {
      throw new Error(`Client error ${res.status}`); // 即 throw
    }
    // 5xx は再試行
    await sleep(2 ** i * 500);
  }
  throw new Error("max retries exceeded");
}

9. 並列数制限 – p-limit パターン

「1000 件の URL を並列に取得したいが、同時実行は 5 件まで」というケース。p-limit ライブラリまたは手書きのセマフォを使います。

9.1 p-limit ライブラリの使用例

// npm install p-limit
import pLimit from "p-limit";

const limit = pLimit(5); // 同時実行 5 件まで

const urls = [...Array(1000)].map((_, i) => `/api/items/${i}`);

const results = await Promise.all(
  urls.map((url) => limit(() => fetch(url).then((r) => r.json())))
);

9.2 自作の並列制限関数

ライブラリを入れたくない場合の最小実装。動作原理を理解するためにも一度書いてみる価値があります。

async function parallelLimit<T, R>(
  items: T[],
  limit: number,
  fn: (item: T, index: number) => Promise<R>
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let index = 0;

  const workers = Array.from({ length: limit }, async () => {
    while (true) {
      const i = index++;
      if (i >= items.length) return;
      results[i] = await fn(items[i], i);
    }
  });

  await Promise.all(workers);
  return results;
}

// 使用例
const results = await parallelLimit(urls, 5, async (url) => {
  const res = await fetch(url);
  return res.json();
});

10. Async Iterator と for await…of

ストリーミング処理やページネーション API では Async Iterator(Symbol.asyncIterator プロトコル)と for await...of 構文が圧倒的にきれいに書けます。

10.1 Async Generator でページネーション

async function* paginate(baseUrl: string) {
  let url: string | null = baseUrl;
  while (url) {
    const res = await fetch(url);
    const json = await res.json();
    yield json.items;
    url = json.next ?? null; // 次のページ URL
  }
}

// 使用例
for await (const items of paginate("/api/posts?page=1")) {
  for (const item of items) {
    console.log(item.title);
  }
}

10.2 Stream を Async Iterator として読む

Fetch API の Response body は ReadableStream であり、Node.js 22+ / モダンブラウザではそのまま for await...of で消費可能です。

const res = await fetch("/api/stream");
const decoder = new TextDecoder();

for await (const chunk of res.body!) {
  const text = decoder.decode(chunk);
  console.log("chunk:", text);
}

10.3 OpenAI のような SSE ストリーミング

ChatGPT API のような Server-Sent Events をフロントで読む実装も、Async Iterator で素直に書けます。

async function* streamCompletion(prompt: string) {
  const res = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ prompt }),
  });
  const decoder = new TextDecoder();
  let buffer = "";

  for await (const chunk of res.body!) {
    buffer += decoder.decode(chunk, { stream: true });
    const lines = buffer.split("n");
    buffer = lines.pop() ?? "";
    for (const line of lines) {
      if (line.startsWith("data: ")) {
        const json = JSON.parse(line.slice(6));
        yield json.delta as string;
      }
    }
  }
}

for await (const delta of streamCompletion("Hello")) {
  process.stdout.write(delta); // 逐次表示
}

11. Top-level await(ESM)

ECMAScript 2022 で標準化された Top-level await は、ES Module の最上位で await を使える機能です。即時実行関数(IIFE)が不要になります。

11.1 IIFE が不要に

// ❌ Before(ES2022 以前)
(async () => {
  const config = await fetch("/config.json").then((r) => r.json());
  startApp(config);
})();

// ✅ After(ES2022+ ESM)
const config = await fetch("/config.json").then((r) => r.json());
startApp(config);

11.2 動的 import との組み合わせ

環境変数や条件で読み込むモジュールを切り替える場合、Top-level await + dynamic import の組み合わせが定番です。

// 環境変数で本番/モック切り替え
const dbModule = process.env.NODE_ENV === "production"
  ? await import("./db.real.js")
  : await import("./db.mock.js");

export const db = dbModule.default;

11.3 Top-level await の注意点

Top-level await はモジュールの読み込みをブロックします。大きすぎる処理を書くとアプリ起動が遅延します。また、CommonJS では使えないため "type": "module" または .mjs 拡張子が必要です。

// ❌ 重すぎる処理を Top-level に書くとアプリ起動が遅い
const allUsers = await fetchAllUsers(); // 数千件取得...

// ✅ 軽い初期化のみに留める
const config = await loadConfig();
// 重い処理は関数として export し、必要時に呼ぶ
export async function loadHeavyData() { /* ... */ }

12. Node.js での async/await 実例

Node.js 22+ の標準 API はすべて Promise ベースに統一されつつあります。コールバック地獄(callback hell)はもはや過去のものです。

12.1 fs/promises – ファイル操作

import { readFile, writeFile, readdir } from "node:fs/promises";

// ファイル読み込み
const text = await readFile("./input.txt", "utf-8");

// 並列に複数ファイル読む
const files = await readdir("./posts");
const contents = await Promise.all(
  files.map((f) => readFile(`./posts/${f}`, "utf-8"))
);

// 書き込み
await writeFile("./output.json", JSON.stringify(contents), "utf-8");

12.2 子プロセスを Promise で扱う

import { promisify } from "node:util";
import { exec } from "node:child_process";

const execAsync = promisify(exec);

const { stdout } = await execAsync("git rev-parse HEAD");
console.log("HEAD:", stdout.trim());

12.3 Express + async/await

Express 5(2024 年安定版)はasync ミドルウェアの例外を自動で next() に渡すようになりました。Express 4 までは express-async-errors パッケージが必要でした。

import express from "express";
const app = express();

app.get("/users/:id", async (req, res, next) => {
  const user = await db.findUser(req.params.id); // throw すると...
  if (!user) return res.status(404).json({ error: "not found" });
  res.json(user);
});

// Express 5 は throw された Error を自動で next(err) に転送する
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: err.message });
});

app.listen(3000);

13. React と async/await

React コンポーネントの関数本体は async にできません(React は同期的にレンダリングするため)。データ取得は useEffect / TanStack Query / Suspense / use() のいずれかで行います。

13.1 useEffect 内の async は IIFE で囲う

// ❌ NG - useEffect のコールバックを async にすると cleanup と型が衝突
useEffect(async () => {
  const data = await fetch(...);
}, []);

// ✅ OK - 内側で async IIFE
useEffect(() => {
  (async () => {
    const data = await fetch(...);
    setData(data);
  })();
}, []);

13.2 イベントハンドラは async OK

function SaveButton({ data }) {
  const [saving, setSaving] = useState(false);

  const handleClick = async () => {
    setSaving(true);
    try {
      await api.save(data);
      toast.success("保存しました");
    } catch (err) {
      toast.error(err.message);
    } finally {
      setSaving(false);
    }
  };

  return <button onClick={handleClick} disabled={saving}>保存</button>;
}

13.3 React 19 の use() フック

React 19 で正式リリースされた use() フックはレンダリング中に Promise を待てる魔法のフックです。Suspense と組み合わせるとデータ取得のローディング処理を宣言的に書けます。詳細は React Suspense 完全ガイド 参照。

import { use, Suspense } from "react";

// Promise を引数に取るコンポーネント
function UserName({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // ← レンダリング中に await
  return <h1>{user.name}</h1>;
}

function App() {
  const userPromise = fetch("/api/me").then((r) => r.json());
  return (
    <Suspense fallback={<p>読み込み中...</p>}>
      <UserName userPromise={userPromise} />
    </Suspense>
  );
}

13.4 Next.js Server Components – async コンポーネント

Next.js App Router の Server Components ではコンポーネント関数そのものを async にできます。クライアントには絶対バンドルされないので、DB 直接アクセス・API キー使用も安全です。

// app/users/[id]/page.tsx (Server Component)
export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await db.user.findUnique({ where: { id: params.id } });
  if (!user) return <p>Not found</p>;
  return (
    <article>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </article>
  );
}

14. TypeScript と async/await

TypeScript では async 関数の戻り値型は必ず Promise<T>になります。型推論は基本的に正しく動きますが、いくつか覚えておくと得をするテクニックがあります。

14.1 戻り値型を明示する

// 戻り値型を書くと「return 忘れ」をコンパイル時に検出できる
async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json(); // ← 型が User と互換でないとエラー
}

// Awaited<T> で Promise の中身の型を抽出
type R = Awaited<ReturnType<typeof getUser>>; // = User

14.2 Promise.all のタプル推論

Promise.all はタプル(配列リテラル)で渡すと各要素の型を保持してくれます。as const は不要です。

const [user, posts] = await Promise.all([
  fetch("/api/me").then((r) => r.json() as Promise<User>),
  fetch("/api/posts").then((r) => r.json() as Promise<Post[]>),
]);

// user: User, posts: Post[] と推論される

14.3 Result 型でエラーを値として扱う

try/catch が冗長になる場合はResult 型(Rust の Result<T, E> に類似)を導入する選択肢があります。エラーが「型として明示される」ので堅牢です。

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
    return { ok: true, value: (await res.json()) as T };
  } catch (err) {
    return { ok: false, error: err as Error };
  }
}

const result = await safeFetch<User>("/api/me");
if (result.ok) {
  console.log(result.value.name);
} else {
  console.error(result.error.message);
}

15. アンチパターン総まとめ

実務でよく見かける async/await のアンチパターンを 8 つまとめました。コードレビューチェックリストとしてご活用ください。

15.1 不要な return await

関数の最後で単に Promise を返す場合、await は不要です。「await して return」より「Promise をそのまま return」の方が 1 マイクロタスク分速く、スタックトレースも短くなります。例外は try ブロック内で、catch に渡したい場合のみ。

// ❌ 冗長
async function getUser(id) {
  return await fetch(`/api/users/${id}`).then((r) => r.json());
}

// ✅ そのまま return
async function getUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// ⚠️ 例外: try/catch 内では return await が必要
async function safe() {
  try {
    return await mightThrow(); // ← await しないと catch されない
  } catch (err) {
    return null;
  }
}

15.2 並列にできるのに直列で書く

// ❌ 直列(遅い)
async function ng() {
  const a = await fetchA();
  const b = await fetchB(); // a に依存していないのに待っている
  return { a, b };
}

// ✅ 並列(速い)
async function ok() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return { a, b };
}

15.3 Promise コンストラクタアンチパターン

すでに Promise を返す関数の戻り値を、わざわざ new Promise でラップしないでください。エラー処理が壊れます。

// ❌ NG
function ng(id) {
  return new Promise((resolve, reject) => {
    fetch(`/api/users/${id}`)
      .then((res) => resolve(res.json()))
      .catch((err) => reject(err));
  });
}

// ✅ OK - そのまま返せばよい
function ok(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

15.4 async を付けただけで await を使わない

// ❌ async 付けただけ(Promise でラップされる副作用だけが残る)
async function ng() {
  return 42;
}

// ✅ 純粋関数なら async は外す
function ok() {
  return 42;
}

16. パフォーマンス最適化のポイント

大量データを扱う場合、async/await の使い方一つでパフォーマンスが 10 倍以上変わることがあります。

16.1 ループの外で await をまとめる

// ❌ 直列(1 件 200ms × 100 件 = 20 秒)
async function slow(ids) {
  const results = [];
  for (const id of ids) {
    results.push(await fetchOne(id));
  }
  return results;
}

// ✅ 並列(全件同時 = 200ms 程度)
async function fast(ids) {
  return Promise.all(ids.map((id) => fetchOne(id)));
}

16.2 マイクロタスクとイベントループ

await が解決すると、その続きはマイクロタスクキューに積まれます。マイクロタスクは現在のタスク終了後・次のレンダリング前にすべて消化されるため、await を多用すると UI ブロッキングを引き起こす場合があります。

// 重い処理は scheduler.yield() / setTimeout で分割
async function processHeavy(items) {
  for (let i = 0; i < items.length; i++) {
    processOne(items[i]);
    if (i % 100 === 0) {
      // 100 件ごとに UI スレッドに譲る
      await new Promise((r) => setTimeout(r, 0));
    }
  }
}

17. テストでの async/await

Vitest / Jest はテスト関数を async にできます。await を忘れるとテストが期待通りに動かないので注意です。

import { describe, it, expect } from "vitest";

describe("getUser", () => {
  it("ユーザーを取得できる", async () => {
    const user = await getUser("1");
    expect(user.name).toBe("Alice");
  });

  it("404 で NotFoundError を投げる", async () => {
    await expect(getUser("999")).rejects.toThrow(NotFoundError);
  });
});

18. async/await 学習のロードマップ

本記事の内容を実務で使いこなすには、段階的な学習が効果的です。独学が難しい場合は体系的なカリキュラムを提供するスクールも選択肢です。

  • STEP 1: Promise 基礎(then/catch/finally) → 本記事の 1〜3 章
  • STEP 2: 並列処理パターン(Promise.all/allSettled/race/any) → 4〜5 章
  • STEP 3: エラー処理・キャンセル・タイムアウト → 6〜8 章
  • STEP 4: Async Iterator / Top-level await / 並列数制限 → 9〜11 章
  • STEP 5: フレームワーク連携(React/Express/Next.js) → 12〜13 章
  • STEP 6: TypeScript 型安全化 → 14 章

独学だと「動けば良し」で終わってしまい、本番運用で初めて落とし穴を踏むことが多々あります。短期間でモダン JS を体系的に学びたい方には、現役エンジニア講師がコードレビューしてくれるオンラインスクールが効率的です。

  • テックアカデミー – JavaScript / フロントエンドコースあり。週 2 回のマンツーマンメンタリングで非同期処理など難所もカバー
  • 侍エンジニア – 完全オーダーメイドカリキュラム。自分のレベル・目標に合わせて async/await・React・Node.js を組み合わせ可能
  • DMM WEBCAMP – 短期集中型。転職保証付きのコースあり。実務レベルのチーム開発演習で非同期処理を扱える
  • レバテックキャリア – 学習よりも転職を目指す中級者向け。モダン JS スキル(async/await・TS・React)を活かせる案件多数

19. よくある質問(FAQ)

Q. async/await と Promise.then はどちらを使うべき?

A. 基本は async/await。読みやすさ・try/catch との統合・条件分岐の自然さで圧倒的に有利です。例外は、配列処理で .map((x) => x.then(...)) のような短い変換の場合に then が簡潔になるケース。

Q. await を付けないとどうなる?

A. 戻り値が Promise オブジェクトのままになります。プロパティアクセスは undefined になり、エラーも catch できません。ESLint の @typescript-eslint/no-floating-promises ルールで検出を強制しましょう。

Q. forEach の中で await できないのはなぜ?

A. forEach はコールバックの戻り値(Promise)を無視する仕様です。直列なら for…of、並列なら Promise.all + map を使います。

Q. Promise.all と Promise.allSettled の使い分けは?

A. 「全件揃わないと意味がない」なら all、「一部失敗しても残りは欲しい」なら allSettled。ユーザー向けダッシュボードの複数 API 並列取得などは allSettled が無難です。

Q. Top-level await は本番で使って大丈夫?

A. ESM("type": "module" または .mjs)なら主要ブラウザ・Node.js 14.8+ で OK。ただしモジュール読み込みをブロックするため、起動時の軽い初期化のみに留めるのが定石です。

Q. キャンセルした fetch はサーバー側で止まる?

A. abort() はクライアント側で接続を切断するだけ。サーバー側は気付かず処理を続けることがあります。Express では req.on("close", ...) で検出可能です。

Q. async/await と Web Worker はどう関係する?

A. Web Worker は別スレッドで処理する仕組みで、worker への postMessage を Promise でラップすれば async/await で扱えます。重い CPU 処理を await で書いても UI ブロッキングが避けられます。

20. まとめ – async/await を制する者は JavaScript を制する

本記事では async/await の基礎から実務テクニックまで、40 以上のコード例で網羅しました。重要ポイントを 7 つにまとめます。

  1. async 関数は必ず Promise を返す。await は async 内 or Top-level でのみ。
  2. 独立処理は Promise.all で並列化。直列ループは「順序保証 / Rate Limit」のときだけ。
  3. forEach + async は NG。for…of(直列)または Promise.all + map(並列)。
  4. エラーは try/catch で同期的に書ける。ただし fetch は HTTP エラーで reject しないので res.ok チェック必須。
  5. キャンセルは AbortController + signal。タイムアウトは AbortSignal.timeout(ms)。
  6. 大量並列は p-limit で同時実行数を制限。サーバーに優しい実装を心がける。
  7. Top-level await は ESM 限定。重い処理を書かない。

async/await は JavaScript の生産性を劇的に高めますが、「動けばいい」を超えて「保守可能で高パフォーマンスな非同期処理」を書くには、本記事のパターン集を実務で使い込む必要があります。

関連記事として、言語全般のベストプラクティスは JavaScript ベストプラクティス 10 選、React 副作用フックの観点は useEffect 完全ガイド、サーバー側型安全化は Zod 完全実践ガイド をご参照ください。

JavaScript を独学で進める方には体系的なオンライン学習サービスが学習効率を大きく上げます。マンツーマンメンタリング付きの テックアカデミー や、オーダーメイドカリキュラムの 侍エンジニア、転職保証の DMM WEBCAMP、案件マッチングの レバテックキャリア など、自分のフェーズに合わせて検討してみてください。

コメント

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