JavaScriptのasync/await

2025-07-28

はじめに

Promiseを学んだことで、非同期処理の扱いが大幅に改善されたことを実感できたと思います。しかし、Promiseチェーンも複雑になると可読性が下がる場合があります。この章では、ES2017で導入された「async/await」について詳しく解説します。async/awaitを使うと、非同期コードを同期コードのように直感的に書けるようになり、Promiseの利点を保ちつつ、さらに可読性を高めることができます。

async/awaitとは?

基本的な定義

async/awaitはPromiseをベースにしたシンタックスシュガー(構文糖)で、非同期コードを同期コードのような見た目で書けるようにする機能です。主に以下の2つのキーワードで構成されます:

  1. async: 関数の前に付けることで、その関数が非同期関数であることを宣言
  2. await: Promiseの前に付けることで、そのPromiseが解決されるまで関数の実行を一時停止

シンプルな例

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('エラーが発生しました:', error);
    throw error;
  }
}

fetchData().then(data => console.log('最終データ:', data));

なぜasync/awaitが必要なのか?

Promiseチェーンの課題

Promiseチェーンも強力ですが、以下のような課題がありました:

  1. 複雑なフローの可読性低下: 複数の非同期処理が絡むとチェーンが長くなる
  2. 変数のスコープ問題: 中間値にアクセスするためにネストが必要
  3. エラー処理の分散: チェーンの途中で個別にエラー処理が必要な場合がある
  4. 条件分岐の煩雑さ: 条件に応じた非同期処理の分岐が書きにくい

async/awaitのメリット

  1. コードの直線的な記述: 同期コードのように上から下に読める
  2. 変数のスコープが自然: 中間結果を簡単に保持できる
  3. try/catchが使える: 同期処理と同じエラー処理が可能
  4. 条件分岐が簡単: if文やループで自然に非同期処理を扱える
  5. デバッグの容易さ: スタックトレースが分かりやすい

async/awaitの基本的な使い方

async関数の宣言

関数の前にasyncキーワードを付けることで、その関数は常にPromiseを返すようになります。

async function myAsyncFunction() {
  return 42; // 自動的にPromiseでラップされる
}

// 上記は以下と同等
function myAsyncFunction() {
  return Promise.resolve(42);
}

await式の使用

awaitはasync関数内でのみ使用でき、Promiseが解決されるまで実行を一時停止します。

async function fetchUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

エラー処理

async/awaitでは、同期処理と同じくtry/catchでエラーを捕捉できます。

async function loadData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error('ネットワークレスポンスが正常ではありません');
    }
    const data = await response.json();
    console.log('データ取得成功:', data);
    return data;
  } catch (error) {
    console.error('データ取得失敗:', error);
    throw error; // 呼び出し元でもエラーを処理できるように
  }
}

実践的な使用例

例1: 複数の非同期処理の直列実行

async function processTasks() {
  const result1 = await task1();
  console.log('task1完了:', result1);

  const result2 = await task2(result1);
  console.log('task2完了:', result2);

  const result3 = await task3(result2);
  console.log('task3完了:', result3);

  return result3;
}

processTasks().catch(error => console.error('処理失敗:', error));

例2: 並列実行と待機

async function fetchAllData() {
  // 並列で開始
  const [users, products, orders] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/products').then(r => r.json()),
    fetch('/api/orders').then(r => r.json())
  ]);

  console.log('すべてのデータ取得完了');
  return { users, products, orders };
}

例3: ループ内での非同期処理

async function processItems(items) {
  const results = [];
  for (const item of items) {
    try {
      const result = await processItem(item);
      results.push(result);
      console.log(`処理完了: ${item.id}`);
    } catch (error) {
      console.error(`${item.id}の処理に失敗:`, error);
    }
  }
  return results;
}

async/awaitの落とし穴

間違い1: 不必要な直列化

// 非効率な例(各awaitで待機)
async function slowExample() {
  const a = await fetchA();
  const b = await fetchB();
  return a + b;
}

// 改善例(並列で実行)
async function fastExample() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return a + b;
}

間違い2: awaitの忘れ

async function example() {
  const promise = fetchData(); // awaitを忘れるとPromiseオブジェクトが返る
  console.log(promise); // Promise {  }
}

間違い3: トップレベルawaitの誤用

// モジュール内でないと使えない(ES2022以降でモジュール内なら可能)
await fetchData(); // エラー

// 正しい使い方
(async () => {
  await fetchData();
})();

async/awaitのベストプラクティス

  1. 常にtry/catchでエラー処理: 捕捉しないと未処理のPromise拒否が発生
  2. 並列処理を意識: 依存のない処理はPromise.allで並列実行
  3. async関数に名前を付ける: スタックトレースを分かりやすく
  4. 適度に関数を分割: 大きすぎるasync関数は分割
  5. ドキュメンテーション: 非同期関数であることをコメントで明記

Promiseとの相互運用

async/awaitはPromiseと完全に互換性があります。

Promiseを返す関数をawaitする

async function example() {
  const data = await fetchData(); // fetchData()はPromiseを返す
}

async関数からPromiseとして扱う

async function getUser() {
  return { name: '太郎' };
}

getUser().then(user => console.log(user));

コールバックと組み合わせる

async function processWithTimeout() {
  return new Promise((resolve) => {
    setTimeout(async () => {
      const data = await fetchData();
      resolve(data);
    }, 1000);
  });
}

高度な使い方

ジェネレータ関数との組み合わせ

async function* asyncGenerator() {
  let i = 0;
  while (i < 3) {
    await delay(100);
    yield i++;
  }
}

(async () => {
  for await (const num of asyncGenerator()) {
    console.log(num);
  }
})();

トップレベルawait (ES2022)

モジュール内ではトップレベルでawaitが使えます。

// module.js
const data = await fetchData();
console.log(data);
export { data };

パフォーマンス考慮事項

  1. マイクロタスクキュー: await毎にマイクロタスクが生成される
  2. メモリ使用量: 大量のawaitはスタックを消費
  3. 並列化の機会を見逃さない: 依存のない処理は並列実行
  4. キャンセル処理: AbortControllerとの組み合わせ

まとめ

async/awaitについての重要なポイント:

  • async/awaitはPromiseを基盤としたシンタックスシュガー
  • 非同期コードを同期コードのように書ける
  • try/catchで直感的なエラー処理が可能
  • async関数は常にPromiseを返す
  • awaitはasync関数内でのみ使用可能
  • Promiseとの相互運用が可能
  • 並列処理にはPromise.allを活用
  • コードの可読性と保守性が大幅に向上

async/awaitをマスターすることで、JavaScriptの非同期プログラミングがより自然で管理しやすいものになります。

練習問題

問題1

以下のコードの実行順序と出力結果を予想してください。

console.log('スクリプト開始');

async function asyncFunc() {
  console.log('asyncFunc開始');
  await new Promise(resolve => setTimeout(resolve, 0));
  console.log('asyncFunc内のawait後');
}

asyncFunc().then(() => console.log('asyncFunc解決'));

new Promise(resolve => {
  console.log('Promise実行');
  resolve();
}).then(() => console.log('Promise解決'));

console.log('スクリプト終了');

問題2

次のasync/awaitの特徴について、正しいものには○、間違っているものには×をつけてください。

  1. async関数は常にPromiseを返す ( )
  2. awaitは通常の関数内でも使用できる ( )
  3. async/awaitを使うとPromiseチェーンよりもスタックトレースが分かりやすくなる ( )
  4. try/catchで非同期処理のエラーを捕捉できる ( )

問題3

以下のPromiseチェーンをasync/awaitを使って書き直してください。

function getUser(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error('ユーザーが見つかりません');
      }
      return response.json();
    })
    .then(user => {
      return fetch(`/api/profile/${user.profileId}`);
    })
    .then(response => response.json())
    .catch(error => {
      console.error('エラー:', error);
      throw error;
    });
}

解答例

問題1の解答

スクリプト開始
asyncFunc開始
Promise実行
スクリプト終了
Promise解決
asyncFunc内のawait後
asyncFunc解決

説明:

  1. 同期的なコードが最初に実行
  2. asyncFunc()が呼ばれ、内部の同期的なコードが実行
  3. Promiseコンストラクタ内の同期的なコードが実行
  4. スクリプトの同期的な実行が終了
  5. マイクロタスクキューにあるPromise解決が実行
  6. awaitの後の処理がマイクロタスクとして実行
  7. asyncFuncの解決ハンドラが実行

問題2の解答

  1. × (async関数内でのみ使用可能)

問題3の解答

async function getUser(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error('ユーザーが見つかりません');
    }
    const user = await response.json();
    const profileResponse = await fetch(`/api/profile/${user.profileId}`);
    return await profileResponse.json();
  } catch (error) {
    console.error('エラー:', error);
    throw error;
  }
}