Promiseとasync/awaitの使い分け:実装パターンで理解する違い

JavaScriptの非同期処理に欠かせないPromiseとasync/awaitの違いを理解することで、読みやすく保守しやすいコードを書くことができます。本記事では、両者の特性を比較し、実践的な使い分けルールを解説します。

JavaScriptの非同期処理の歴史と位置づけ

JavaScriptで非同期処理が必要になる場面は、API呼び出しやファイル読み込み、データベースアクセスなど多岐にわたります。これらの処理結果を待つ必要がありますが、ブロッキングすると画面が固まるため、非同期で処理する仕組みが生まれました。

最初はコールバック関数で対応していましたが、ネストが深くなる「コールバック地獄」という問題が発生。これを解決するためにPromiseが登場し、さらに読みやすい記法としてasync/awaitが導入されました。async/awaitはPromiseの上に構築された糖衣構文であり、根本的には同じ仕組みを使っています。

Promiseの基本:チェーン可能な非同期処理

Promiseの基本構文と3つの状態

Promiseは「pending(待機中)」「fulfilled(成功)」「rejected(失敗)」の3つの状態を持ちます。一度状態が決まると、二度と変わることはありません。

// Promiseの基本形
const myPromise = new Promise((resolve, reject) => {
  // 時間がかかる処理
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('処理が成功しました'); // fulfilled状態へ
    } else {
      reject('エラーが発生しました'); // rejected状態へ
    }
  }, 1000);
});

// Promiseを使う側
myPromise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('処理終了'));

Promiseチェーンで複数の非同期処理を繋ぐ

実務では複数のAPIを順序立てて呼び出す必要があります。Promiseチェーンはこのような場面で活躍します。

// ユーザー情報を取得 → ユーザーの投稿を取得 → コメントを取得
fetch('/api/user/123')
  .then(response => response.json())
  .then(user => {
    console.log('ユーザー:', user);
    return fetch(`/api/posts?userId=${user.id}`);
  })
  .then(response => response.json())
  .then(posts => {
    console.log('投稿:', posts);
    return fetch(`/api/comments?postId=${posts[0].id}`);
  })
  .then(response => response.json())
  .then(comments => console.log('コメント:', comments))
  .catch(error => console.error('エラー:', error));

Promise.all()で複数の処理を並列実行

処理の順序が不要な場合、Promise.all()で並列実行するとパフォーマンスが向上します。

// 3つのAPI呼び出しを並列実行
Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
])
  .then(([users, posts, comments]) => {
    console.log('全データ取得完了', { users, posts, comments });
  })
  .catch(error => console.error('いずれかのAPIが失敗:', error));

async/awaitの実力:同期処理のような書き方

async/awaitの基本と利点

async/awaitを使うと、非同期処理を同期処理のような形で書けます。コードの読みやすさが大幅に向上し、ロジックの追跡が容易になります。

// async/awaitで記述
async function getUserWithPosts(userId) {
  try {
    // awaitで各処理の完了を待つ
    const userResponse = await fetch(`/api/user/${userId}`);
    const user = await userResponse.json();
    console.log('ユーザー:', user);

    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsResponse.json();
    console.log('投稿:', posts);

    return { user, posts };
  } catch (error) {
    console.error('エラー:', error);
  }
}

// 関数の呼び出し
getUserWithPosts(123).then(result => console.log(result));

async関数は常にPromiseを返す

重要なポイント:async関数は必ずPromiseを返します。async/awaitを使っていても、内部的にはPromiseで動作しており、呼び出し側ではPromiseとして扱う必要があります。

// async関数は自動的にPromiseでラップされる
async function fetchData() {
  return 'データ'; // Promiseでラップされる
}

// 呼び出し側
const result = fetchData(); // これはPromise
console.log(result instanceof Promise); // true

result.then(value => console.log(value)); // 'データ'

並列実行でasync/awaitの威力が発揮される

複数の非同期処理を並列実行する場合、async/awaitの記述方法がPromiseチェーンより直感的です。

// 【良い例】Promise.all()を使った並列実行
async function getMultipleData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json())
    ]);
    console.log({ users, posts, comments });
  } catch (error) {
    console.error('エラー:', error);
  }
}

// 【悪い例】順序実行になってしまう
async function getMultipleDataBad() {
  const users = await fetch('/api/users').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json()); // 待機中
  const comments = await fetch('/api/comments').then(r => r.json()); // さらに待機
  // 実行時間が3倍以上かかる可能性
}

PromiseとAsync/Awaitの使い分けガイド

よくあるハマりポイント:エラーハンドリングの違い

Promiseとasync/awaitではエラーハンドリングの方法が異なります。これを理解しないと、エラーが意図せず無視される状況が発生します。

// 【Promiseの場合】catchを忘れるとエラーが無視される
fetch('/api/data')
  .then(r => r.json())
  .then(data => console.log(data));
  // .catch()がないと、エラーが検出されない

// 【async/awaitの場合】try-catchでキャッチ必須
async function getData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('エラーをキャッチ:', error);
  }
}

// 【重要】HTTPエラーはawaitでは自動判定されない
async function safeGetData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('エラー:', error);
  }
}

使い分けの判断基準

Promiseを使うべき場面:

  • チェーン的な非同期処理の連結が多い場合
  • Promise.race()やPromise.any()など高度なPromise操作が必要な場合
  • 既存のPromiseベースのコードを拡張する場合
  • ブラウザの互換性が極めて重要な場合(async/awaitはES2017以降)

async/awaitを使うべき場面(推奨):

  • コードの可読性を最優先したい場合(ほとんどの実務シーン)
  • 複数の非同期処理の制御が複雑な場合
  • デバッグやログ出力を挟みたい場合
  • try-catchでエラー処理を統一したい場合
  • 同期的な思考フローで開発したい場合

実務的な併用パターン

// 【実践的なパターン】async/awaitとPromise.all()の組み合わせ
async function processUserData(userId) {
  try {
    // ユーザー情報は必ず先に取得
    const userResponse = await fetch(`/api/user/${userId}`);
    const user = await userResponse.json();

    // その後の関連データは並列取得
    const [posts, followers, settings] = await Promise.all([
      fetch(`/api/posts?userId=${userId}`).then(r => r.json()),
      fetch(`/api/followers?userId=${userId}`).then(r => r.json()),
      fetch(`/api/settings?userId=${userId}`).then(r => r.json())
    ]);

    return { user, posts, followers, settings };
  } catch (error) {
    console.error('データ取得失敗:', error);
    throw error; // 上位で処理する場合
  }
}

// リトライロジックの例
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error; // 最後の試行で例外を投げる
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // 指数バックオフ
    }
  }
}

パフォーマンスの違いと最適化

JavaScriptエンジンレベルではPromiseとasync/awaitの性能差はほぼありません。両者ともEvent Loopで同じように処理されます。重要なのは「並列実行か順序実行か」という実装パターンです。

// 【パフォーマンス比較】

// ❌ 遅い:順序実行(3秒待機)
async function slowVersion() {
  const a = await api1(); // 1秒
  const b = await api2(); // 1秒
  const c = await api3(); // 1秒
  return { a, b, c }; // 計3秒
}

// ✅ 速い:並列実行(1秒待機)
async function fastVersion() {
  const [a, b, c] = await Promise.all([
    api1(),
    api2(),
    api3()
  ]); // 計1秒
  return { a, b, c };
}

よくある質問

A: ほぼそうですが、完全ではありません。Promise.race()やPromise.any()などの高度な操作が必要な場合、Promiseを直接使う必要があります。また、Node.jsやブラウザのバージョンによってはasync/awaitが利用できない場合があります。

A: awaitは各反復で完了を待つため、順序実行になります。複数のPromiseを並列実行したい場合は、Promise.all()を使うか、先にすべてのPromiseを作成してから await Promise.all() で待つ必要があります。

A: async関数は自動的にPromiseを返すため、return文で値を返すだけでPromiseでラップされます。ただし、呼び出し側でPromiseとして扱う必要がある場合、明示的にPromiseを返すことを推奨します。

まとめ

  • async/awaitはPromiseの糖衣構文であり、内部的には同じEvent Loopで動作している
  • async/awaitはコード可読性が優れており、ほとんどの実務で推奨される
  • 並列実行にはPromise.all()を組み合わせることで、パフォーマンスを大幅に向上できる
  • エラーハンドリングはtry-catchで統一することが可読性を保つコツ
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →