Promise/async-await

非同期処理の結果を表現するオブジェクトと、それを同期的な記法で扱うための構文

非同期JavaScript

Promise/async-await とは

Promise は、非同期処理の最終的な完了 (または失敗) とその結果値を表現するオブジェクトである。async/await は Promise を同期的なコードのように記述できる構文糖で、ES2017 で導入された。コールバックの入れ子 (Callback Hell) を解消し、処理の順序とエラーの合流点をコードの見た目どおりに書けるようにした。

コールバック → Promise → async/await の進化

3 つの書き方はどれも同じ処理を並べたものだが、違いはエラーの扱いに出る。コールバックは呼び出しごとに err を検査するしかなく、1 か所忘れれば失敗が黙って先へ進む。Promise チェーンは末尾の catch、async/await は try/catch に失敗の合流点を 1 つ作れる。

// ❌ コールバック地獄: ネストが深く、エラーハンドリングが散在
getUser(id, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    getPayment(orders[0].id, (err, payment) => {
      if (err) return handleError(err);
      console.log(payment);
    });
  });
});

// ✅ Promise チェーン: フラットだがまだ冗長
getUser(id)
  .then(user => getOrders(user.id))
  .then(orders => getPayment(orders[0].id))
  .then(payment => console.log(payment))
  .catch(handleError);

// ✅ async/await: 同期コードのように読める
async function processPayment(id: string) {
  const user = await getUser(id);
  const orders = await getOrders(user.id);
  const payment = await getPayment(orders[0].id);
  console.log(payment);
}

Promise の 3 つの状態

Promise は pending から fulfilled か rejected へ一度だけ遷移する。この 2 つをまとめて settled (決着済み) と呼ぶ。

pending (保留中) ──→ fulfilled (成功) → .then() で値を取得
                 └→ rejected (失敗)  → .catch() でエラーを取得

決着すると結果が固定されるため、決着した後に then を付けてもハンドラーは呼ばれる。非同期処理の完了とハンドラーの登録が競合しないのはこの性質のおかげで、コールバックのように「登録が間に合わず通知を取り逃がす」事故が起きない。

もう 1 つ押さえておきたいのが再開のタイミングである。await は後続の処理をマイクロタスクとして予約する。マイクロタスクはイベントループがタイマーや I/O のコールバックへ進む前にまとめて処理されるので、await の直後の行は setTimeout(..., 0) より先に走る。

並行実行パターン

await を縦に並べると 1 つずつ待つことになる。互いに依存しない処理は Promise を先にまとめて作り、待つ操作を 1 回にする。どのメソッドを選ぶかは「1 つの失敗で全体を諦めるか」で決まる。

// Promise.all: 全部成功したら結果を返す。1 つでも失敗したら即 reject
const [user, orders, config] = await Promise.all([
  getUser(id),
  getOrders(id),
  getConfig(),
]);

// Promise.allSettled: 全部完了するまで待つ (成功・失敗を問わない)
const results = await Promise.allSettled([
  sendEmail(user),
  sendSlack(user),
  sendSms(user),
]);
// results: [{ status: 'fulfilled', value: ... }, { status: 'rejected', reason: ... }]

// Promise.race: 最初に完了した結果を返す (タイムアウトに使える)
const result = await Promise.race([
  fetchData(),
  timeout(5000),  // 5秒でタイムアウト
]);
メソッド成功条件失敗条件用途
Promise.all全部成功1 つでも失敗全データが必要な場合
Promise.allSettled常に成功全体が失敗することはなく、個々の成否が結果の配列に入る部分的な失敗を許容
Promise.race最初の完了最初の失敗タイムアウト
Promise.any最初の成功全部失敗フォールバック

よくあるバグ

await の付け忘れ

// ❌ await がない: promise オブジェクトが返る (値ではない)
const user = getUser(id);  // Promise<User> が返る
console.log(user.name);     // undefined (Promise に name プロパティはない)

// ✅ await で値を取り出す
const user = await getUser(id);  // User が返る
console.log(user.name);           // 正しい値

TypeScript なら user.name は型エラーで気づけるが、戻り値が any に落ちている箇所や素の JavaScript では undefined のまま処理が進む。より厄介なのは失敗したときで、await を付けずに呼んだ非同期関数が投げたエラーは呼び出し側の try/catch を通らない。Node.js は 15.0 以降、こうした未処理の rejection を既定で捕捉されない例外として扱うため、プロセスがそのまま停止する。@typescript-eslint/no-floating-promises ルールで検出できる。

直列実行の非効率

// ❌ 直列: 各 await が前の完了を待つ (遅い)
const user = await getUser(id);      // 100ms
const orders = await getOrders(id);  // 100ms
const config = await getConfig();    // 100ms
// 合計: 300ms

// ✅ 並行: 独立した処理は Promise.all で同時実行
const [user, orders, config] = await Promise.all([
  getUser(id),     // 100ms
  getOrders(id),   // 100ms
  getConfig(),     // 100ms
]);
// 合計: 100ms (最も遅い処理の時間)

エラーの握りつぶし

// ❌ catch で何もしない → エラーが消える
try { await riskyOperation(); } catch (e) { /* 何もしない */ }

// ✅ エラーをログに記録し、必要なら再スロー
try { await riskyOperation(); } catch (e) {
  logger.error('Operation failed', e);
  throw e;
}

使い分けの目安は単純である。順番に意味がある処理は await を縦に並べ、独立した処理は Promise.all でまとめる。失敗しても続けたい通知系は Promise.allSettled を選ぶ。そして await を書かない非同期呼び出しを残さないことが、静かに壊れるコードを避ける最短の手当てになる。

この記事は役に立ちましたか?

関連用語

関連する記事