JavaScript async/awaitの実践的な使い方|実務パターンとサンプルコード集

JavaScript

JavaScript async/awaitの実践的な使い方|実務パターンとサンプルコード集

async/awaitとは|簡易的な解説

JavaScript の async/await は、非同期処理を同期的なコードのように書ける構文です。Promise ベースの API をより読みやすく、扱いやすくしてくれます。

async キーワードを関数の前に付けると、その関数は自動的に Promise を返すようになります。await キーワードを使うと、Promise が解決されるまで処理を一時停止できます。

従来のコールバック地獄や複雑な then チェーンから解放され、エラーハンドリングも try-catch で統一できるため、保守性が大幅に向上します。

業務でのユースケース

実務では以下のようなシーンで async/await が活躍します:

  • REST API の呼び出し:複数のエンドポイントへのリクエスト管理
  • データベース操作:クエリの順序制御と結果の依存管理
  • ファイル操作:読み込み、書き込みの連鎖処理
  • リトライロジック:失敗時の自動再試行
  • 並列処理の制御:複数の非同期処理の効率的な実行

特に、バックエンド API との連携やデータフェッチが頻繁なWebアプリケーションでは、async/await がなくては成り立たないほど重要です。

実装コード|実務で使えるサンプル集

1. 基本的な API 呼び出しパターン

まず最も基本的なユースケースです。ユーザー情報を取得し、そのユーザーの投稿一覧を取得するという依存関係のある処理をします。

// 実務パターン:ユーザー情報取得→投稿一覧取得
async function fetchUserWithPosts(userId) {
  try {
    // ユーザー情報を取得
    const userResponse = await fetch(`/api/users/${userId}`);
    if (!userResponse.ok) {
      throw new Error(`ユーザー取得失敗: ${userResponse.status}`);
    }
    const user = await userResponse.json();

    // そのユーザーの投稿を取得(ユーザー情報に依存)
    const postsResponse = await fetch(`/api/users/${userId}/posts`);
    if (!postsResponse.ok) {
      throw new Error(`投稿取得失敗: ${postsResponse.status}`);
    }
    const posts = await postsResponse.json();

    return { user, posts };
  } catch (error) {
    console.error('データ取得エラー:', error.message);
    // ユーザーフレンドリーなエラーメッセージを返す
    throw new Error('ユーザー情報の取得に失敗しました');
  }
}

// 使用例
fetchUserWithPosts(123)
  .then(data => console.log('取得成功:', data))
  .catch(error => console.error('エラー:', error.message));

2. 複数の API 並列呼び出し

複数の独立した API を効率的に並列実行するパターンです。順序に依存しないリクエストはこの方法で高速化できます。

// 実務パターン:複数の API を並列実行
async function fetchDashboardData(userId) {
  try {
    // 3つの API を同時に実行(順序不問)
    const [userData, statistics, notifications] = await Promise.all([
      fetch(`/api/users/${userId}`).then(r => r.json()),
      fetch(`/api/stats/${userId}`).then(r => r.json()),
      fetch(`/api/notifications/${userId}`).then(r => r.json())
    ]);

    return { userData, statistics, notifications };
  } catch (error) {
    console.error('ダッシュボード読み込みエラー:', error);
    throw error;
  }
}

// Promise.allSettled を使う方法(1つ失敗してもキャッチしない)
async function fetchDashboardDataSafe(userId) {
  const results = await Promise.allSettled([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/stats/${userId}`).then(r => r.json()),
    fetch(`/api/notifications/${userId}`).then(r => r.json())
  ]);

  const dashboard = {
    userData: results[0].status === 'fulfilled' ? results[0].value : null,
    statistics: results[1].status === 'fulfilled' ? results[1].value : null,
    notifications: results[2].status === 'fulfilled' ? results[2].value : null
  };

  return dashboard;
}

3. リトライロジック付き API 呼び出し

ネットワークエラーや一時的な障害に対応するため、自動リトライ機能を実装します。

// 実務パターン:リトライ機能付き fetch
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  const { retryDelay = 1000, backoffMultiplier = 2, ...fetchOptions } = options;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, fetchOptions);
      if (!response.ok && response.status >= 500) {
        // サーバーエラーの場合はリトライ
        throw new Error(`Server error: ${response.status}`);
      }
      return response;
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      // 指数バックオフでウェイト
      const delay = retryDelay * Math.pow(backoffMultiplier, attempt - 1);
      console.log(`リトライ ${attempt}/${maxRetries}. ${delay}ms 後に再実行...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// 使用例
async function getDataWithRetry() {
  try {
    const response = await fetchWithRetry('/api/data', {}, 5);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('最大リトライ回数後も失敗:', error);
  }
}

4. TypeScript での型安全な実装

実務では TypeScript を使うことが増えています。async/await に型情報を加えた実装です。

// TypeScript での実装
interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  userId: number;
  title: string;
  content: string;
}

interface ApiResponse {
  success: boolean;
  data: T;
  error?: string;
}

async function fetchUserWithPostsTs(userId: number): Promise<{ user: User; posts: Post[] }> {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    const userData: ApiResponse = await userResponse.json();

    if (!userData.success) {
      throw new Error(userData.error || 'User fetch failed');
    }

    const postsResponse = await fetch(`/api/users/${userId}/posts`);
    const postsData: ApiResponse = await postsResponse.json();

    if (!postsData.success) {
      throw new Error(postsData.error || 'Posts fetch failed');
    }

    return {
      user: userData.data,
      posts: postsData.data
    };
  } catch (error) {
    console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
    throw error;
  }
}

// 使用例(型チェック付き)
async function main(): Promise {
  const result = await fetchUserWithPostsTs(1);
  // result は { user: User; posts: Post[] } 型で確定
  console.log(result.user.name, result.posts.length);
}

5. Python での参考実装

JavaScript の async/await は Python の asyncio と非常に似ています。参考として Python での実装も示します。

import asyncio
import aiohttp
from typing import Dict, List, Any

# Python での async/await 実装
async def fetch_user_with_posts_py(user_id: int) -> Dict[str, Any]:
    async with aiohttp.ClientSession() as session:
        try:
            # ユーザー情報を取得
            async with session.get(f'/api/users/{user_id}') as user_response:
                if user_response.status != 200:
                    raise Exception(f'User fetch failed: {user_response.status}')
                user = await user_response.json()

            # 投稿一覧を取得
            async with session.get(f'/api/users/{user_id}/posts') as posts_response:
                if posts_response.status != 200:
                    raise Exception(f'Posts fetch failed: {posts_response.status}')
                posts = await posts_response.json()

            return {'user': user, 'posts': posts}
        except Exception as error:
            print(f'Error: {error}')
            raise

# 複数の並列実行
async def fetch_dashboard_data_py(user_id: int) -> Dict[str, Any]:
    async with aiohttp.ClientSession() as session:
        try:
            results = await asyncio.gather(
                session.get(f'/api/users/{user_id}').then(r => r.json()),
                session.get(f'/api/stats/{user_id}').then(r => r.json()),
                session.get(f'/api/notifications/{user_id}').then(r => r.json()),
                return_exceptions=True
            )
            return results
        except Exception as error:
            print(f'Error: {error}')
            raise

# 実行
asyncio.run(fetch_user_with_posts_py(1))

よくある応用パターン

1. タイムアウト処理の実装

長時間応答がない API に対して強制的にタイムアウトさせる必要があります。

// タイムアウト機能付き fetch
async function fetchWithTimeout(url, options = {}) {
  const { timeout = 5000, ...fetchOptions } = options;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...fetchOptions,
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeout}ms`);
    }
    throw error;
  }
}

// 使用例
async function getDataWithTimeout() {
  try {
    const response = await fetchWithTimeout('/api/slow-endpoint', { timeout: 3000 });
    const data = await response.json();
    console.log('データ取得成功:', data);
  } catch (error) {
    console.error('タイムアウトまたはエラー:', error.message);
  }
}

2. キューイングと処理制限

同時実行数を制限して API レート制限に対応します。

// 同時実行数制限付きキュー実装
class AsyncQueue {
  constructor(concurrency = 2) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  async add(fn) {
    while (this.running >= this.concurrency) {
      await new Promise(resolve => this.queue.push(resolve));
    }

    this.running++;
    try {
      return await fn();
    } finally {
      this.running--;
      const resolve = this.queue.shift();
      if (resolve) resolve();
    }
  }
}

// 使用例:100個の API 呼び出しを最大 3 つ並列実行
const queue = new AsyncQueue(3);
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);

const tasks = userIds.map(userId =>
  queue.add(() => fetch(`/api/users/${userId}`).then(r => r.json()))
);

const allUsers = await Promise.all(tasks);
console.log('全ユーザー取得完了:', allUsers.length);

3. キャッシング機能

同じリクエストを複数回実行するのを避けるためのキャッシュ実装です。

// キャッシング機能付き関数
class CachedAsyncFunction {
  constructor(fn, cacheDuration = 60000) {
    this.fn = fn;
    this.cacheDuration = cacheDuration;
    this.cache = new Map();
  }

  async call(...args) {
    const key = JSON.stringify(args);
    const cached = this.cache.get(key);

    if (cached && Date.now() - cached.timestamp < this.cacheDuration) {
      console.log('キャッシュから取得:', key);
      return cached.value;
    }

    const value = await this.fn(...args);
    this.cache.set(key, { value, timestamp: Date.now() });
    return value;
  }
}

// 使用例
const cachedFetchUser = new CachedAsyncFunction(
  async (userId) => {
    console.log(`API呼び出し: userId=${userId}`);
    return fetch(`/api/users/${userId}`).then(r => r.json());
  },
  5000 // 5秒キャッシュ
);

// 1回目は API 呼び出し、2回目はキャッシュから取得
await cachedFetchUser.call(1);
await cachedFetchUser.call(1); // キャッシュから取得

注意点と落とし穴

1. await を忘れる

最も多いバグです。Promise を await せずに使うと、非同期処理の完了を待たずに次の処理へ進みます。

// ❌ 間違い:await を忘れている
async function wrongExample() {
  const response = fetch('/api/data'); // await なし
  const data = response.json(); // Promise オブジェクト
  console.log(data); // Promise {  } が出力される
}

// ✅ 正しい:await を付ける
async function correctExample() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data); // 実際のデータが出力される
}

2. エラーハンドリングの不足

async/await ではエラーが try-catch で捕捉されないと、呼び出し元へ未処理の Promise rejection として伝播します。

// ❌ 間違い:エラーハンドリングなし
async function unsafeFunction() {
  const data = await fetch('/api/data').then(r => r.json());
  return data;
}

unsafeFunction(); // エラーが発生するとコンソールに警告が出る

// ✅ 正しい:適切なエラーハンドリング
async function safeFunction() {
  try {
    const data = await fetch('/api/data').then(r => r.json());
    return data;
  } catch (error) {
    console.error('エラーが発生しました:', error);
    // エラーを処理するか、より詳細なエラーをスロー
    throw new Error('データ取得に失敗しました');
  }
}

3. 不要な逐次処理

独立した処理を順番に await していると、無駄に時間がかかります。

// ❌ 遅い:逐次処理(3秒かかる)
async function slowExample() {
  const user = await fetch('/api/users/1').then(r => r.json()); // 1秒
  const posts = await fetch('/api/posts/1').then(r => r.json()); // 1秒
  const comments = await fetch('/api/comments/1').then(r => r.json()); // 1秒
  return { user, posts, comments };
}

// ✅ 速い:並列処理(1秒で完了)
async function fastExample() {
  const [user, posts, comments] = await Promise.all([
    fetch('/api/users/1').then(r => r.json()),
    fetch('/api/posts/1').then(r => r.json()),
    fetch('/api/comments/1').then(r => r.json())
  ]);
  return { user, posts, comments };
}

4. メモリリーク対策

長時間実行される async 関数は、リソースのリークに注意が必要です。

// ❌ 危ない:リスナーが増殖する可能性
async function setupListener() {
  const controller = new AbortController();
  window.addEventListener('message', (event) => {
    // 処理
  });
  // abort しても listener は残ったまま
}

// ✅ 安全:リスナーを適切にクリーンアップ
async function setupListenerSafe() {
  const controller = new AbortController();
  const handler = (event) => {
    // 処理
  };
  window.addEventListener('message', handler);

  // クリーンアップ関数を返す
  return () => {
    window.removeEventListener('message', handler);
    controller.abort();
  };
}

// 使用時
const cleanup = await setupListenerSafe();
// 不要になったら
cleanup();

まとめ

JavaScriptの async/await は、非同期処理を直感的に書ける強力な機能です。実務では以下のポイントが重要です:

  • 基本をマスターする:async/await の基本的な使い方を確実に理解する
  • エラーハンドリングは必須:try-catch で適切にエラー処理を行う
  • 並列処理を活用する:独立した処理は Promise.all で高速化
  • リトライとタイムアウト:実務では必ず実装すべき機能
  • 型安全性:TypeScript を使って型チェックを活用する
  • メモリ管理:長時間実行関数ではリソースリークに注意

これらを意識することで、保守性が高く、バグの少ないコードが書けます。実務の様々なユースケースに対応できる async/await マスターになりましょう。

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