はじめに

JavaScriptで非同期処理を書くとき、「Promise」と「async/await」という2つの書き方を目にすることがあります。どちらも非同期処理を扱う仕組みですが、async/awaitを使うと、まるで同期処理のように直感的なコードを書けるようになります。

ES2017(ES8)で導入されたasync/awaitは、Promiseをベースにした構文糖衣(シンタックスシュガー)です。Promiseチェーンの複雑さを解消し、可読性の高いコードを実現します。

本記事では、以下の内容を初心者向けにわかりやすく解説します。

  • async/awaitの基本構文と動作原理
  • Promiseとの違いと使い分け
  • try-catchによるエラーハンドリング
  • 複数の非同期処理を並列実行する方法
  • 実践的なAPI通信の例

async/awaitとは

基本概念

async/awaitは、非同期処理を同期処理のように書けるJavaScript の構文です。asyncキーワードを関数に付けると「非同期関数」になり、その中でawaitキーワードを使ってPromiseの完了を待つことができます。

1
2
3
4
5
6
// async関数の基本形
async function fetchData() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
}

上記のコードでは、fetchresponse.json()という2つの非同期処理を順番に実行しています。awaitにより、各処理が完了するまで次の行に進まないため、同期処理のように上から下へ読めます。

asyncキーワードの役割

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// async関数は必ずPromiseを返す
async function greet() {
  return "Hello!";
}

// 上記は以下と等価
function greet() {
  return Promise.resolve("Hello!");
}

// 戻り値の確認
greet().then((message) => {
  console.log(message); // "Hello!"
});

async関数内でreturnした値は、自動的にPromise.resolve()でラップされます。

awaitキーワードの役割

awaitキーワードは、Promiseが解決(fulfilled)または拒否(rejected)されるまで処理を一時停止します。Promiseが解決されると、その結果値が返されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function example() {
  console.log("1. 開始");
  
  // 1秒待機するPromiseをawait
  await new Promise((resolve) => setTimeout(resolve, 1000));
  
  console.log("2. 1秒後に実行");
  
  // さらに1秒待機
  await new Promise((resolve) => setTimeout(resolve, 1000));
  
  console.log("3. 2秒後に実行");
}

example();
console.log("4. async関数を呼び出した直後");

// 出力順序:
// 1. 開始
// 4. async関数を呼び出した直後
// 2. 1秒後に実行(1秒後)
// 3. 2秒後に実行(2秒後)

awaitasync関数の中でのみ使用可能です。通常の関数内で使うとSyntaxErrorが発生します。

1
2
3
4
// エラー: awaitはasync関数内でのみ使用可能
function normalFunction() {
  await fetch("https://api.example.com/data"); // SyntaxError
}

ただし、ES2022以降では、モジュールのトップレベルでawaitを使用できる「Top-level await」がサポートされています。

1
2
3
4
// ESモジュールのトップレベルでのawait(ES2022以降)
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);

Promiseとasync/awaitの比較

同じ処理の書き方の違い

Promiseチェーンとasync/awaitで同じ処理を書いた場合の違いを見てみましょう。

Promiseチェーンを使った書き方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function fetchUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTP error: ${response.status}`);
      }
      return response.json();
    })
    .then((user) => {
      return fetch(`https://api.example.com/posts?userId=${user.id}`);
    })
    .then((response) => response.json())
    .then((posts) => {
      console.log("ユーザーの投稿:", posts);
      return posts;
    })
    .catch((error) => {
      console.error("エラー:", error);
    });
}

async/awaitを使った書き方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async function fetchUserData(userId) {
  try {
    const userResponse = await fetch(`https://api.example.com/users/${userId}`);
    if (!userResponse.ok) {
      throw new Error(`HTTP error: ${userResponse.status}`);
    }
    const user = await userResponse.json();

    const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
    const posts = await postsResponse.json();

    console.log("ユーザーの投稿:", posts);
    return posts;
  } catch (error) {
    console.error("エラー:", error);
  }
}

async/await版は、処理の流れが上から下へ一直線に読めるため、可読性が大幅に向上しています。

比較表

観点 Promise(.then/.catch) async/await
可読性 チェーンが長いと複雑になりやすい 同期的なコードのように読みやすい
エラー処理 .catch()で処理 try-catchで処理
デバッグ スタックトレースが追いにくい 通常の関数と同様に追いやすい
条件分岐 ネストが深くなりやすい 通常のif文で記述可能
互換性 ES2015(ES6)以降 ES2017(ES8)以降

処理フローの図解

async/await使用時の処理フローを図で確認してみましょう。

sequenceDiagram
    participant Main as メインスレッド
    participant API as API Server
    
    Main->>Main: async関数を呼び出し
    Main->>API: await fetch() - リクエスト送信
    Note over Main: 処理を一時停止<br/>(他のコードは実行可能)
    API-->>Main: レスポンス返却
    Main->>Main: await response.json()
    Note over Main: 処理を一時停止
    Main->>Main: JSON解析完了
    Main->>Main: 次の処理を実行

エラーハンドリング

try-catchによる基本的なエラー処理

async/awaitでは、同期処理と同様にtry-catch文でエラーを捕捉できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
async function fetchData() {
  try {
    const response = await fetch("https://api.example.com/data");
    
    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }
    
    const data = await response.json();
    console.log("取得成功:", data);
    return data;
  } catch (error) {
    console.error("エラーが発生しました:", error.message);
    return null;
  }
}

finallyによるクリーンアップ処理

finallyブロックを使うと、成功・失敗に関わらず必ず実行したい処理を記述できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
async function loadData() {
  const loadingIndicator = document.getElementById("loading");
  
  try {
    loadingIndicator.style.display = "block"; // ローディング表示
    
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("データの取得に失敗しました:", error);
    throw error;
  } finally {
    loadingIndicator.style.display = "none"; // ローディング非表示(必ず実行)
  }
}

エラーの種類に応じた処理

エラーの種類を判別して、適切な対応を行うことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
async function fetchWithRetry(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);
      
      if (!response.ok) {
        // HTTPステータスに応じたエラー処理
        if (response.status === 404) {
          throw new Error("リソースが見つかりません");
        } else if (response.status === 401) {
          throw new Error("認証が必要です");
        } else if (response.status >= 500) {
          throw new Error("サーバーエラー");
        }
      }
      
      return await response.json();
    } catch (error) {
      console.log(`試行 ${attempt}/${maxRetries} 失敗:`, error.message);
      
      if (error.name === "TypeError") {
        // ネットワークエラー(fetch自体の失敗)
        console.log("ネットワーク接続を確認してください");
      }
      
      if (attempt === maxRetries) {
        throw new Error(`${maxRetries}回の試行後も失敗: ${error.message}`);
      }
      
      // リトライ前に待機
      await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
    }
  }
}

エラー処理のベストプラクティス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// カスタムエラークラスの定義
class ApiError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = "ApiError";
    this.statusCode = statusCode;
  }
}

// エラーを適切に処理する関数
async function fetchApi(endpoint) {
  try {
    const response = await fetch(`https://api.example.com${endpoint}`);
    
    if (!response.ok) {
      throw new ApiError(
        `API request failed: ${response.statusText}`,
        response.status
      );
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof ApiError) {
      // APIエラーの場合
      console.error(`API Error (${error.statusCode}):`, error.message);
    } else if (error instanceof TypeError) {
      // ネットワークエラーの場合
      console.error("Network Error:", error.message);
    } else {
      // その他のエラー
      console.error("Unexpected Error:", error);
    }
    throw error; // エラーを再スロー
  }
}

複数の非同期処理の実行パターン

逐次実行(順番に実行)

前の処理の結果を次の処理で使う場合は、順番にawaitします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async function sequentialExecution() {
  console.log("開始");
  
  const user = await fetchUser(1);        // 1秒かかると仮定
  const posts = await fetchPosts(user.id); // 1秒かかると仮定
  const comments = await fetchComments(posts[0].id); // 1秒かかると仮定
  
  console.log("完了"); // 合計3秒後
  return { user, posts, comments };
}

並列実行(Promise.all)

互いに依存しない処理は、Promise.allで並列に実行すると効率的です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
async function parallelExecution() {
  console.log("開始");
  
  // 3つのリクエストを同時に開始
  const [users, products, orders] = await Promise.all([
    fetch("https://api.example.com/users").then((r) => r.json()),
    fetch("https://api.example.com/products").then((r) => r.json()),
    fetch("https://api.example.com/orders").then((r) => r.json()),
  ]);
  
  console.log("完了"); // 最も遅いリクエストが完了した時点
  return { users, products, orders };
}
gantt
    title 並列実行 vs 逐次実行
    dateFormat X
    axisFormat %s秒
    
    section 逐次実行
    ユーザー取得   :0, 1
    商品取得       :1, 2
    注文取得       :2, 3
    
    section 並列実行
    ユーザー取得   :0, 1
    商品取得       :0, 1
    注文取得       :0, 1

Promise.allSettled(すべての結果を取得)

Promise.allは1つでも失敗すると全体が失敗扱いになりますが、Promise.allSettledはすべての結果(成功・失敗両方)を取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function fetchAllData() {
  const results = await Promise.allSettled([
    fetch("https://api.example.com/users").then((r) => r.json()),
    fetch("https://invalid-url.com/data").then((r) => r.json()), // 失敗する
    fetch("https://api.example.com/products").then((r) => r.json()),
  ]);
  
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`リクエスト${index + 1} 成功:`, result.value);
    } else {
      console.log(`リクエスト${index + 1} 失敗:`, result.reason);
    }
  });
  
  // 成功した結果のみを抽出
  const successfulResults = results
    .filter((r) => r.status === "fulfilled")
    .map((r) => r.value);
  
  return successfulResults;
}

Promise.race(最初に完了した結果を取得)

Promise.raceは、最初に完了(成功または失敗)したPromiseの結果を返します。タイムアウト処理の実装などに使えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// タイムアウト付きのfetch
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("タイムアウト")), timeoutMs);
  });
  
  const fetchPromise = fetch(url).then((r) => r.json());
  
  return Promise.race([fetchPromise, timeoutPromise]);
}

// 使用例
async function getData() {
  try {
    const data = await fetchWithTimeout("https://api.example.com/data", 3000);
    console.log("データ取得成功:", data);
  } catch (error) {
    if (error.message === "タイムアウト") {
      console.log("リクエストがタイムアウトしました");
    } else {
      console.log("エラー:", error.message);
    }
  }
}

Promise.any(最初に成功した結果を取得)

Promise.anyは、最初に成功したPromiseの結果を返します。すべて失敗した場合のみエラーになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
async function fetchFromMultipleSources() {
  try {
    // 複数のミラーサーバーから最初に成功したものを使用
    const data = await Promise.any([
      fetch("https://server1.example.com/data").then((r) => r.json()),
      fetch("https://server2.example.com/data").then((r) => r.json()),
      fetch("https://server3.example.com/data").then((r) => r.json()),
    ]);
    console.log("データ取得成功:", data);
    return data;
  } catch (error) {
    console.error("すべてのサーバーから取得に失敗しました");
    throw error;
  }
}

実践的なコード例

APIからデータを取得して表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ユーザー一覧を取得して表示する実践例
async function displayUsers() {
  const container = document.getElementById("user-list");
  const errorMessage = document.getElementById("error-message");
  
  try {
    // ローディング表示
    container.innerHTML = "<p>読み込み中...</p>";
    
    // APIからユーザー一覧を取得
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    
    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status}`);
    }
    
    const users = await response.json();
    
    // HTMLを生成して表示
    const html = users
      .map(
        (user) => `
        <div class="user-card">
          <h3>${user.name}</h3>
          <p>Email: ${user.email}</p>
          <p>会社: ${user.company.name}</p>
        </div>
      `
      )
      .join("");
    
    container.innerHTML = html;
  } catch (error) {
    errorMessage.textContent = `データの取得に失敗しました: ${error.message}`;
    container.innerHTML = "";
  }
}

// ページ読み込み時に実行
document.addEventListener("DOMContentLoaded", displayUsers);

フォーム送信処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
async function handleFormSubmit(event) {
  event.preventDefault();
  
  const form = event.target;
  const submitButton = form.querySelector('button[type="submit"]');
  const formData = new FormData(form);
  
  // ボタンを無効化
  submitButton.disabled = true;
  submitButton.textContent = "送信中...";
  
  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(Object.fromEntries(formData)),
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || "送信に失敗しました");
    }
    
    const result = await response.json();
    
    // 成功メッセージを表示
    alert("送信が完了しました!");
    form.reset();
  } catch (error) {
    alert(`エラー: ${error.message}`);
  } finally {
    // ボタンを有効化
    submitButton.disabled = false;
    submitButton.textContent = "送信";
  }
}

document.getElementById("contact-form").addEventListener("submit", handleFormSubmit);

データのキャッシュ処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 簡易的なキャッシュ機能付きのデータ取得
const cache = new Map();

async function fetchWithCache(url, cacheTimeMs = 60000) {
  const cached = cache.get(url);
  
  // キャッシュが有効な場合はキャッシュを返す
  if (cached && Date.now() - cached.timestamp < cacheTimeMs) {
    console.log("キャッシュからデータを取得");
    return cached.data;
  }
  
  console.log("APIからデータを取得");
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  
  const data = await response.json();
  
  // キャッシュに保存
  cache.set(url, {
    data,
    timestamp: Date.now(),
  });
  
  return data;
}

// 使用例
async function loadProducts() {
  const products = await fetchWithCache("https://api.example.com/products");
  console.log(products);
}

ループ内でのasync/await

配列の各要素に対して非同期処理を行う場合の注意点です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 良い例: for...of を使った逐次処理
async function processItemsSequentially(items) {
  const results = [];
  
  for (const item of items) {
    const result = await processItem(item); // 1つずつ順番に処理
    results.push(result);
  }
  
  return results;
}

// 良い例: Promise.all を使った並列処理
async function processItemsInParallel(items) {
  const results = await Promise.all(
    items.map((item) => processItem(item)) // すべて同時に処理
  );
  
  return results;
}

// 悪い例: forEachはawaitを正しく待たない
async function processItemsBad(items) {
  const results = [];
  
  // forEachはasync関数を待たない
  items.forEach(async (item) => {
    const result = await processItem(item);
    results.push(result); // 順序が保証されない
  });
  
  console.log(results); // 空の配列が出力される可能性
}

よくある間違いと注意点

1. awaitの忘れ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 間違い: awaitを忘れている
async function fetchData() {
  const response = fetch("https://api.example.com/data"); // awaitがない
  const data = response.json(); // responseはPromiseオブジェクト
  return data;
}

// 正しい
async function fetchData() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
}

2. async関数の戻り値はPromise

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 間違い: async関数の戻り値を直接使おうとしている
async function getData() {
  return { name: "太郎" };
}

const data = getData();
console.log(data.name); // undefined(dataはPromiseオブジェクト)

// 正しい
const data = await getData(); // async関数内で使用
console.log(data.name); // "太郎"

// または
getData().then((data) => {
  console.log(data.name); // "太郎"
});

3. 不要な逐次実行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 非効率: 独立した処理を順番に実行している
async function fetchAllData() {
  const users = await fetchUsers();      // 1秒
  const products = await fetchProducts(); // 1秒
  const orders = await fetchOrders();     // 1秒
  // 合計3秒かかる
}

// 効率的: 並列で実行
async function fetchAllData() {
  const [users, products, orders] = await Promise.all([
    fetchUsers(),
    fetchProducts(),
    fetchOrders(),
  ]);
  // 約1秒で完了
}

4. エラーハンドリングの忘れ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 危険: エラーハンドリングがない
async function riskyFunction() {
  const data = await fetch("https://api.example.com/data");
  return data.json();
}

// 安全: try-catchでエラーを処理
async function safeFunction() {
  try {
    const response = await fetch("https://api.example.com/data");
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("エラー:", error);
    return null;
  }
}

async/awaitを使うべき場面

async/awaitは以下のような場面で特に効果を発揮します。

場面 理由
複数の非同期処理を順番に実行する場合 Promiseチェーンよりも読みやすい
条件分岐を含む非同期処理 通常のif文で書ける
ループ内での非同期処理 for…ofと組み合わせて直感的に書ける
エラーハンドリングが複雑な場合 try-catchで統一的に処理できる
デバッグが必要な場合 スタックトレースが追いやすい

まとめ

async/awaitは、JavaScriptの非同期処理を直感的に書くための強力な構文です。本記事で解説した内容をまとめます。

  • async関数は必ずPromiseを返す
  • awaitはPromiseの完了を待ち、結果を取得する
  • エラー処理はtry-catchで行う
  • 独立した複数の非同期処理はPromise.allで並列実行する
  • forEach内でのawaitは期待通りに動作しないため、for...ofを使う

async/awaitをマスターすれば、API通信やファイル読み込みなど、さまざまな非同期処理を効率的に記述できるようになります。まずは本記事のコード例を実際に動かして、感覚をつかんでみてください。

参考リンク