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/awaitを使ってAPIからデータを取得する非同期処理の基本例です。
fetchData関数内でfetchを呼び出し、awaitでレスポンスの取得とJSONへの変換を順に待機します。処理中にエラーが発生した場合はtry...catchで補足し、エラーメッセージを表示したうえで再スローします。関数はPromiseを返すため、呼び出し側では.then()を使って最終的なデータを受け取り表示します。

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が必要なのか?

async/awaitを利用するメリットを以下のように定義します。

Promiseチェーンの課題

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

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

Promiseについては次のページで詳細に説明しています。

async/awaitのメリット

async/awaitを利用することで以下のメリットを享受できます。

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

async/awaitの基本的な使い方

async関数の宣言

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

async function myAsyncFunction()で定義された関数は、return 42と書くだけでPromise.resolve(42)と同等の動作をし、常にPromiseオブジェクトを返します。つまり、asyncを付けることで、関数の戻り値が自動的に非同期処理として扱われるようになります。

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

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

await式の使用

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

fetch('/api/user')でAPIにリクエストを送り、awaitでレスポンスの取得とJSONデータへの変換を順に待機します。最終的に取得したuserオブジェクトを返し、この関数はPromiseを返す非同期関数として動作します。

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

エラー処理

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

fetch('/api/data')でリクエストを行い、レスポンスのokプロパティをチェックしてエラーハンドリングを行います。レスポンスが正常ならawait response.json()でデータを取得し、コンソールに出力します。エラー発生時は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; // 呼び出し元でもエラーを処理できるように
  }
}

実践的な使用例

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

async関数内でawaitを複数記述すると、非同期処理を順次実行できます。
前の処理の結果を次の処理で使用する場合に有効で、コードの可読性が向上します。

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));

並列実行と待機

Promise.all()とawaitを組み合わせることで、複数の非同期処理を並列実行し、すべての完了を待機できます。処理時間の短縮と効率化が図れます。

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 };
}

ループ内での非同期処理

forループ内でawaitを使用すると、各反復処理を同期的に実行できます。
非同期処理の完了を待ってから次の反復に進むため、順序が保証されます。

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の落とし穴(間違い)

不必要な直列化

依存関係のない非同期処理を個別にawaitすると、不要な待機時間が発生します。
Promise.all()で並列実行することで処理効率が向上します。

// 非効率な例(各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;
}

awaitの忘れ

async関数内でPromiseを返す関数をawaitせずに呼び出すと、Promiseオブジェクトが解決される前に次の処理が実行され、意図しない動作の原因となります。

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

トップレベルawaitの誤用

モジュール外でトップレベルawaitを使用すると構文エラーになります。
ES2022ではモジュール内でのみ使用可能で、適切なスコープ管理が必要です。

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

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

Promiseとの相互運用

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

Promiseを返す関数をawaitする

example関数はasyncで定義されており、fetchData()が返すPromiseの完了をawaitで待ってからdataに結果を代入します。これにより、非同期処理を同期的な流れで記述でき、可読性が向上します。

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

async関数からPromiseとして扱う

getUser関数は{ name: '太郎' }というオブジェクトを返していますが、実際にはPromise.resolve({ name: '太郎' })と同等に扱われます。呼び出し側では.then()を使って結果を受け取り、console.logでユーザー情報を出力します。

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

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

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

次のコードは、1秒後にfetchData()を実行し、その結果を返す非同期処理の例です。
processWithTimeout関数はPromiseを返し、setTimeout内でawait fetchData()を使ってデータ取得が完了するのを待ちます。その後、resolve(data)で結果を返します。asyncPromiseを組み合わせることで、時間遅延を伴う非同期処理を柔軟に制御しています。

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

高度な使い方

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

次のコードは、**非同期イテレーター(async generator)**の基本的な使い方を示しています。
async function*で定義されたasyncGeneratorは、awaitを使いながら値を順にyieldで返す非同期ジェネレーター関数です。

for await...of構文を使うことで、各yieldの値が非同期的に生成されるたびに順に受け取り、console.log(num)で出力します。これにより、時間を伴う逐次的な非同期処理をシンプルに表現できます。

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)

次のコードは、**ESモジュール(ECMAScript Modules)**でトップレベルawaitを使用する例です。

module.js内では関数外でもawaitが使えるため、fetchData()の非同期処理が完了するまで待ってから結果をdataに代入し、コンソールに出力します。その後、dataを他のモジュールにexportして再利用できるようにしています。

この構文は、ESモジュール環境(type="module"指定など)でのみ有効です。

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

パフォーマンス考慮事項

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

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

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

まとめ

async/awaitはPromiseを基盤とした構文で、非同期処理をあたかも同期処理のように書けるため、コードの可読性と保守性を高めます。async関数は常にPromiseを返し、awaitはその中でのみ使えます。

エラー処理はtry/catchで直感的に行え、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;
  }
}