T-CREATOR

【入門】JavaScript の非同期処理を完全理解!Promise・async/await の基礎と実践

【入門】JavaScript の非同期処理を完全理解!Promise・async/await の基礎と実践

JavaScriptを学習していく中で、必ず出会うのが「非同期処理」という概念です。最初は難しく感じられるかもしれませんが、現代のWebアプリケーション開発において非同期処理は必要不可欠な技術です。

本記事では、JavaScriptの非同期処理について、同期処理との違いから始まり、コールバック関数、Promise、そして最新のasync/awaitまで、段階的にわかりやすく解説していきます。初心者の方でも理解できるよう、豊富なコード例と図解を交えながら説明していきますので、ぜひ最後まで読んでいただければと思います。

同期処理と非同期処理の基本

同期処理とは

同期処理とは、コードが上から順番に実行され、一つの処理が完了するまで次の処理に進まない実行方式のことです。日常生活に例えるなら、料理を作る際に「野菜を切る → 炒める → 調味料を加える」という工程を一つずつ順番に行う作業に似ています。

以下のような基本的なJavaScriptコードは同期処理で実行されます。

javascriptconsole.log('処理1: 開始');
console.log('処理2: 計算中');
console.log('処理3: 終了');

このコードを実行すると、必ず上から順番に出力されます。

makefile処理1: 開始
処理2: 計算中
処理3: 終了

同期処理の特徴は予測可能で理解しやすいことですが、時間のかかる処理(ファイル読み込み、ネットワーク通信など)があると、その処理が完了するまで後続の処理がすべて停止してしまうという問題があります。

非同期処理とは

非同期処理とは、ある処理の完了を待たずに次の処理を実行する方式のことです。料理の例で言えば、「野菜を炒めながら、同時にスープを温める」といった並行作業に相当します。

JavaScriptで非同期処理の代表例として、setTimeout関数があります。

javascriptconsole.log('処理1: 開始');

setTimeout(() => {
  console.log('処理2: 2秒後に実行');
}, 2000);

console.log('処理3: すぐに実行');

この場合の実行結果は以下のようになります。

makefile処理1: 開始
処理3: すぐに実行
(2秒後)
処理2: 2秒後に実行

setTimeoutの処理を待たずに「処理3」が先に実行されているのがわかります。これが非同期処理の基本的な動作です。

以下の図は、同期処理と非同期処理の実行フローの違いを表しています。

mermaidflowchart TD
    subgraph sync["同期処理"]
        A1[処理1] --> A2[時間のかかる処理] --> A3[処理3]
        A2 -.-> wait[他の処理は待機]
    end
    
    subgraph async["非同期処理"]
        B1[処理1] --> B2[非同期処理開始]
        B2 --> B3[処理3]
        B2 -.-> B4[バックグラウンドで実行]
        B4 -.-> B5[コールバック実行]
    end

同期処理では全ての処理が順次実行されるのに対し、非同期処理では時間のかかる処理をバックグラウンドで実行しながら、他の処理を継続できます。

なぜ非同期処理が必要なのか

現代のWebアプリケーションでは、ユーザーの操作に対してリアルタイムで応答する必要があります。もしすべての処理が同期的に実行されてしまうと、以下のような問題が発生します。

問題点具体例影響
UIのブロッキングAPI通信中にボタンが反応しないユーザビリティの低下
レスポンス性の悪化画像読み込み中に画面が固まるユーザーの離脱率増加
処理効率の低下複数のAPIを順次呼び出し全体の処理時間増加

非同期処理を適切に使うことで、これらの問題を解決し、スムーズなユーザーエクスペリエンスを提供できます。具体的には、以下のような場面で非同期処理が活用されています。

  • API通信: サーバーからデータを取得する間も、ユーザーは他の操作を継続できる
  • ファイル処理: 大きなファイルの読み込み中も、アプリケーションは応答を続ける
  • タイマー処理: 定期的な更新処理を背景で実行しながら、メイン機能を提供する

従来のコールバック関数

コールバック関数の基本

JavaScriptで非同期処理を実装する最も基本的な方法が、コールバック関数を使った手法です。コールバック関数とは、ある処理が完了した後に呼び出される関数のことです。

まず、シンプルなコールバック関数の例から見てみましょう。

javascript// 基本的なコールバック関数
function greetUser(name, callback) {
  console.log(`こんにちは、${name}さん!`);
  callback();
}

function afterGreeting() {
  console.log('挨拶が完了しました');
}

// 使用例
greetUser('田中', afterGreeting);

この例では、greetUser関数の第二引数にコールバック関数を渡し、挨拶の処理が完了した後に呼び出しています。

実際の非同期処理でのコールバック関数の使用例を見てみましょう。

javascript// 非同期処理でのコールバック関数
function fetchUserData(userId, callback) {
  console.log(`ユーザーID: ${userId} のデータを取得中...`);
  
  // 実際のAPI通信をシミュレート
  setTimeout(() => {
    const userData = {
      id: userId,
      name: '田中太郎',
      email: 'tanaka@example.com'
    };
    
    callback(userData);
  }, 1000);
}

// コールバック関数の定義
function displayUser(user) {
  console.log('ユーザー情報:');
  console.log(`名前: ${user.name}`);
  console.log(`メール: ${user.email}`);
}

// 実行
fetchUserData(123, displayUser);
console.log('他の処理も並行して実行されます');

このコードを実行すると、以下のような出力が得られます。

makefileユーザーID: 123 のデータを取得中...
他の処理も並行して実行されます
(1秒後)
ユーザー情報:
名前: 田中太郎
メール: tanaka@example.com

コールバック関数を使うことで、データの取得処理を待たずに他の処理を継続できることがわかります。

コールバック地獄の問題

コールバック関数は便利ですが、複数の非同期処理を連続して実行する必要がある場合、コードが複雑になってしまう問題があります。これを「コールバック地獄」と呼びます。

例えば、「ユーザー情報を取得 → そのユーザーの投稿一覧を取得 → 投稿の詳細情報を取得」という連続した処理を考えてみましょう。

javascript// コールバック地獄の例
function fetchUser(userId, callback) {
  setTimeout(() => {
    console.log('ユーザー情報を取得しました');
    callback({ id: userId, name: '田中太郎' });
  }, 1000);
}

function fetchPosts(userId, callback) {
  setTimeout(() => {
    console.log('投稿一覧を取得しました');
    callback([{ id: 1, title: '最初の投稿' }, { id: 2, title: '二番目の投稿' }]);
  }, 1000);
}

function fetchPostDetail(postId, callback) {
  setTimeout(() => {
    console.log('投稿詳細を取得しました');
    callback({ id: postId, content: '投稿の詳細内容です' });
  }, 1000);
}

これらの関数を使って連続処理を実装すると、以下のようなネストした構造になってしまいます。

javascript// 深いネスト構造(コールバック地獄)
fetchUser(123, (user) => {
  console.log(`ユーザー: ${user.name}`);
  
  fetchPosts(user.id, (posts) => {
    console.log(`投稿数: ${posts.length}`);
    
    fetchPostDetail(posts[0].id, (detail) => {
      console.log(`詳細: ${detail.content}`);
      
      // さらに処理が続く場合...
      // ネストがどんどん深くなる
    });
  });
});

このコードの問題点は以下の通りです。

問題点説明影響
可読性の低下ネストが深くなり、コードが読みにくい開発効率の低下
保守性の悪化修正や機能追加が困難バグの発生リスク増加
エラーハンドリングの複雑化各段階でのエラー処理が煩雑信頼性の低下

実際のコード例

実際の開発現場でよく見られる、ファイル読み込みとデータ処理の例を見てみましょう。

javascript// 設定ファイルを読み込んで、データベースに接続し、ユーザー情報を取得する例
function readConfig(callback) {
  console.log('設定ファイルを読み込み中...');
  setTimeout(() => {
    const config = { dbUrl: 'localhost:5432', timeout: 5000 };
    callback(null, config);
  }, 500);
}

function connectDatabase(config, callback) {
  console.log('データベースに接続中...');
  setTimeout(() => {
    const connection = { status: 'connected', config: config };
    callback(null, connection);
  }, 800);
}

function queryUser(connection, userId, callback) {
  console.log('ユーザー情報を検索中...');
  setTimeout(() => {
    const user = { id: userId, name: '山田花子', age: 28 };
    callback(null, user);
  }, 600);
}

エラーハンドリングを含めた完全な実装例は以下のようになります。

javascript// エラーハンドリングを含む連続処理
function getUserInfo(userId) {
  readConfig((error, config) => {
    if (error) {
      console.error('設定読み込みエラー:', error);
      return;
    }
    
    connectDatabase(config, (error, connection) => {
      if (error) {
        console.error('データベース接続エラー:', error);
        return;
      }
      
      queryUser(connection, userId, (error, user) => {
        if (error) {
          console.error('ユーザー検索エラー:', error);
          return;
        }
        
        console.log('処理完了:', user);
      });
    });
  });
}

// 実行
getUserInfo(456);

このように、エラーハンドリングを加えるとさらにコードが複雑になり、保守が困難になってしまいます。こうした問題を解決するために、Promiseという新しい仕組みが導入されました。

Promiseの基礎

Promiseとは

Promiseは、非同期処理の結果を表現するオブジェクトです。ES6(ES2015)で正式に導入され、コールバック地獄の問題を解決するために設計されました。

Promiseは「将来の値」を表現し、非同期処理が成功するか失敗するかに関わらず、その結果を統一的に扱うことができます。料理の例で言えば、「注文票」のようなもので、料理が完成したら通知を受け取れるシステムと考えることができます。

基本的なPromiseの作成方法を見てみましょう。

javascript// 基本的なPromiseの作成
const myPromise = new Promise((resolve, reject) => {
  // 非同期処理をここに記述
  const success = true; // 処理の成功/失敗を判定
  
  if (success) {
    resolve('処理が成功しました!'); // 成功時に呼び出す
  } else {
    reject('処理が失敗しました...'); // 失敗時に呼び出す
  }
});

Promiseコンストラクタは、resolverejectという2つのパラメータを持つ関数を受け取ります。

  • resolve: 処理が成功したときに呼び出す関数
  • reject: 処理が失敗したときに呼び出す関数

Promise の3つの状態

Promiseオブジェクトは常に以下の3つの状態のいずれかにあります。

状態説明次の状態への変化
pending(保留中)初期状態、まだ完了も失敗もしていないfulfilled または rejected
fulfilled(履行済み)処理が成功して完了した状態変化しない(不変)
rejected(拒否済み)処理が失敗した状態変化しない(不変)

以下の図は、Promiseの状態遷移を表しています。

mermaidstateDiagram-v2
    [*] --> pending : Promiseオブジェクト作成
    pending --> fulfilled : resolve()呼び出し
    pending --> rejected : reject()呼び出し
    fulfilled --> [*] : 処理完了
    rejected --> [*] : エラー処理完了
    
    note right of pending : 処理実行中
    note right of fulfilled : 成功した結果を保持
    note right of rejected : エラー情報を保持

重要なのは、一度fulfilledまたはrejected状態になったPromiseは、二度と状態が変わらないということです。この不変性により、予測可能で安全な非同期処理が実現できます。

実際の例で状態の変化を確認してみましょう。

javascript// Promiseの状態変化を確認する例
function createPromise(shouldSucceed) {
  return new Promise((resolve, reject) => {
    console.log('Promise作成: pending状態');
    
    setTimeout(() => {
      if (shouldSucceed) {
        resolve('成功した結果');
        console.log('Promise状態: fulfilled');
      } else {
        reject('エラーが発生');
        console.log('Promise状態: rejected');
      }
    }, 1000);
  });
}

// 成功パターン
createPromise(true);

// 失敗パターン
createPromise(false);

then、catch、finallyメソッド

Promiseの結果を処理するために、3つの主要なメソッドが用意されています。

thenメソッド

thenメソッドは、Promiseが成功(fulfilled)したときの処理を定義します。

javascript// thenメソッドの基本的な使い方
const promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('データ取得完了');
  }, 1000);
});

promise.then((result) => {
  console.log('成功:', result); // "成功: データ取得完了"
});

thenメソッドは2つの引数を取ることができます。第1引数は成功時のコールバック、第2引数は失敗時のコールバックです。

javascript// then メソッドで成功と失敗の両方を処理
promise.then(
  (result) => {
    console.log('成功:', result);
  },
  (error) => {
    console.log('失敗:', error);
  }
);

catchメソッド

catchメソッドは、Promiseが失敗(rejected)したときの処理を定義します。エラーハンドリングを明確にするため、thenの第2引数よりもcatchメソッドの使用が推奨されています。

javascript// catchメソッドの使用例
const failingPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('ネットワークエラーが発生しました');
  }, 1000);
});

failingPromise
  .then((result) => {
    console.log('成功:', result);
  })
  .catch((error) => {
    console.log('エラーをキャッチ:', error);
  });

finallyメソッド

finallyメソッドは、Promiseが成功・失敗に関わらず、必ず実行したい処理を定義します。

javascript// finallyメソッドの使用例
function fetchData() {
  console.log('データ取得開始');
  
  return new Promise((resolve, reject) => {
    const random = Math.random();
    setTimeout(() => {
      if (random > 0.5) {
        resolve('データ取得成功');
      } else {
        reject('データ取得失敗');
      }
    }, 1000);
  });
}

fetchData()
  .then((result) => {
    console.log('結果:', result);
  })
  .catch((error) => {
    console.log('エラー:', error);
  })
  .finally(() => {
    console.log('処理完了 - 必ず実行されます');
  });

Promise チェーン

Promiseの最も強力な機能の一つが、メソッドチェーンです。複数の非同期処理を順次実行する際に、コールバック地獄を回避してスッキリとしたコードが書けます。

javascript// Promise チェーンの基本例
function step1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('ステップ1完了');
      resolve('step1の結果');
    }, 1000);
  });
}

function step2(previousResult) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('ステップ2完了、前の結果:', previousResult);
      resolve('step2の結果');
    }, 1000);
  });
}

function step3(previousResult) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('ステップ3完了、前の結果:', previousResult);
      resolve('最終結果');
    }, 1000);
  });
}

これらの関数をPromiseチェーンで繋げると、以下のようになります。

javascript// 美しいPromiseチェーン
step1()
  .then((result1) => {
    return step2(result1);
  })
  .then((result2) => {
    return step3(result2);
  })
  .then((finalResult) => {
    console.log('全ての処理完了:', finalResult);
  })
  .catch((error) => {
    console.log('どこかでエラーが発生:', error);
  });

さらに簡潔に書くこともできます。

javascript// よりシンプルな書き方
step1()
  .then(step2)
  .then(step3)
  .then((finalResult) => {
    console.log('全ての処理完了:', finalResult);
  })
  .catch((error) => {
    console.log('エラーが発生:', error);
  });

実際のWeb開発でよく見られる、API連続呼び出しの例を見てみましょう。

javascript// 実際のAPI連続呼び出しの例
function fetchUser(userId) {
  return new Promise((resolve, reject) => {
    console.log(`ユーザー${userId}の情報を取得中...`);
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: '田中太郎' });
      } else {
        reject('無効なユーザーIDです');
      }
    }, 1000);
  });
}

function fetchUserPosts(user) {
  return new Promise((resolve) => {
    console.log(`${user.name}さんの投稿を取得中...`);
    setTimeout(() => {
      resolve({
        user: user,
        posts: ['投稿1', '投稿2', '投稿3']
      });
    }, 1000);
  });
}

function generateReport(userData) {
  return new Promise((resolve) => {
    console.log('レポートを生成中...');
    setTimeout(() => {
      resolve({
        userName: userData.user.name,
        postCount: userData.posts.length,
        generatedAt: new Date().toISOString()
      });
    }, 500);
  });
}

// 実行
fetchUser(123)
  .then(fetchUserPosts)
  .then(generateReport)
  .then((report) => {
    console.log('レポート完成:', report);
  })
  .catch((error) => {
    console.log('処理中にエラーが発生:', error);
  });

Promiseチェーンの重要なポイントは、チェーンのどこかでエラーが発生すると、即座にcatchブロックに処理が移ることです。これにより、エラーハンドリングが一箇所に集約され、コードがより保守しやすくなります。

async/await の登場

async/await とは

async/awaitは、ES2017(ES8)で導入された、Promiseをより直感的に扱うための構文です。非同期処理を同期処理のように書くことができるため、コードの可読性が大幅に向上します。

async/awaitの基本的な考え方は、「非同期処理を同期処理のような見た目で書く」ことです。Promiseのチェーンを使わずに、まるで通常の関数のように記述できます。

まず、基本的な構文から見てみましょう。

javascript// async/await の基本構文
async function fetchData() {
  // await キーワードでPromiseの完了を待つ
  const result = await someAsyncFunction();
  return result;
}

async キーワード

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

javascript// async関数の例
async function greet() {
  return 'こんにちは!';
}

// async関数は常にPromiseを返す
console.log(greet()); // Promise { 'こんにちは!' }

// 結果を取得するには then を使う
greet().then((message) => {
  console.log(message); // "こんにちは!"
});

await キーワード

awaitキーワードは、Promiseの完了を待つために使用します。awaitasync関数の中でのみ使用できます。

javascript// awaitの基本的な使い方
async function example() {
  console.log('処理開始');
  
  // Promiseの完了を待つ
  const result = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('非同期処理完了');
    }, 2000);
  });
  
  console.log(result);
  console.log('すべて完了');
}

example();

async 関数の基本

async関数は様々な方法で定義できます。それぞれの書き方を確認してみましょう。

javascript// 1. 関数宣言
async function functionDeclaration() {
  return 'function declaration';
}

// 2. 関数式
const functionExpression = async function() {
  return 'function expression';
};

// 3. アロー関数
const arrowFunction = async () => {
  return 'arrow function';
};

// 4. オブジェクトのメソッド
const obj = {
  async method() {
    return 'object method';
  }
};

// 5. クラスのメソッド
class MyClass {
  async method() {
    return 'class method';
  }
}

async関数の重要な特徴を実際の例で確認してみましょう。

javascript// async関数の戻り値は常にPromise
async function getValue() {
  return 42;
}

// 呼び出し方法
getValue().then((value) => {
  console.log(value); // 42
});

// または別のasync関数内で
async function main() {
  const value = await getValue();
  console.log(value); // 42
}

await キーワードの使い方

awaitキーワードを使うことで、Promiseの結果を直接変数に代入できます。これにより、非同期処理が同期処理のように見えるコードが書けます。

javascript// Promiseチェーンと await の比較

// Promiseチェーンを使った書き方
function fetchUserPromise(userId) {
  return fetchUser(userId)
    .then((user) => {
      console.log('ユーザー情報:', user);
      return fetchPosts(user.id);
    })
    .then((posts) => {
      console.log('投稿一覧:', posts);
      return generateSummary(posts);
    })
    .then((summary) => {
      console.log('サマリー:', summary);
      return summary;
    });
}

// async/await を使った書き方
async function fetchUserAsync(userId) {
  const user = await fetchUser(userId);
  console.log('ユーザー情報:', user);
  
  const posts = await fetchPosts(user.id);
  console.log('投稿一覧:', posts);
  
  const summary = await generateSummary(posts);
  console.log('サマリー:', summary);
  
  return summary;
}

async/awaitを使った方が、処理の流れが直感的に理解できることがわかります。

複数の非同期処理を順次実行する実践的な例を見てみましょう。

javascript// データベース操作のシミュレーション
async function simulateDbOperation(operation, data, delay = 1000) {
  console.log(`${operation}を実行中...`);
  
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.1) { // 90%の確率で成功
        resolve(`${operation}完了: ${JSON.stringify(data)}`);
      } else {
        reject(`${operation}でエラーが発生しました`);
      }
    }, delay);
  });
}

// async/await を使った複数のデータベース操作
async function processUserData(userData) {
  try {
    // ユーザー作成
    const createResult = await simulateDbOperation('ユーザー作成', userData, 800);
    console.log(createResult);
    
    // プロフィール作成
    const profileData = { userId: userData.id, bio: 'よろしくお願いします' };
    const profileResult = await simulateDbOperation('プロフィール作成', profileData, 600);
    console.log(profileResult);
    
    // 初期設定の作成
    const settingsData = { userId: userData.id, theme: 'light', language: 'ja' };
    const settingsResult = await simulateDbOperation('設定作成', settingsData, 400);
    console.log(settingsResult);
    
    console.log('すべての処理が完了しました!');
    return '処理成功';
    
  } catch (error) {
    console.error('処理中にエラーが発生:', error);
    throw error;
  }
}

// 実行
processUserData({ id: 123, name: '山田太郎', email: 'yamada@example.com' });

エラーハンドリング(try-catch)

async/awaitでのエラーハンドリングは、通常のJavaScriptと同じようにtry-catch文を使用します。これにより、エラー処理がより直感的になります。

javascript// try-catch を使ったエラーハンドリング
async function fetchDataWithErrorHandling() {
  try {
    const data = await fetchData();
    console.log('データ取得成功:', data);
    return data;
  } catch (error) {
    console.error('データ取得失敗:', error);
    // エラーを再スローするか、デフォルト値を返すか決める
    return null;
  }
}

複数の非同期処理でのエラーハンドリングの例を見てみましょう。

javascript// 複数の処理でのエラーハンドリング
async function complexOperation() {
  try {
    // 段階1: 設定データの取得
    console.log('設定データを取得中...');
    const config = await fetchConfig();
    
    // 段階2: APIの初期化
    console.log('APIを初期化中...');
    const api = await initializeAPI(config);
    
    // 段階3: ユーザーデータの取得
    console.log('ユーザーデータを取得中...');
    const userData = await fetchUserData(api);
    
    // 段階4: データの加工
    console.log('データを加工中...');
    const processedData = await processData(userData);
    
    return processedData;
    
  } catch (error) {
    // どの段階でエラーが発生しても、ここで捕捉される
    console.error('処理中にエラーが発生しました:', error);
    
    // エラーの種類に応じて適切な処理を行う
    if (error.name === 'NetworkError') {
      console.log('ネットワークエラーのため、リトライを検討してください');
    } else if (error.name === 'ValidationError') {
      console.log('データ検証エラーのため、入力内容を確認してください');
    } else {
      console.log('予期しないエラーが発生しました');
    }
    
    throw error; // 必要に応じてエラーを再スロー
  }
}

特定の処理だけエラーハンドリングを行いたい場合の例も見てみましょう。

javascript// 部分的なエラーハンドリング
async function partialErrorHandling() {
  let userData = null;
  let preferences = {};
  
  try {
    // 必須のユーザーデータ取得
    userData = await fetchUserData();
    console.log('ユーザーデータ取得成功');
  } catch (error) {
    console.error('ユーザーデータの取得に失敗しました');
    throw error; // これは必須なので、エラーを再スロー
  }
  
  try {
    // オプションの設定データ取得(失敗してもデフォルト値を使用)
    preferences = await fetchUserPreferences(userData.id);
    console.log('設定データ取得成功');
  } catch (error) {
    console.warn('設定データの取得に失敗、デフォルト値を使用:', error.message);
    preferences = { theme: 'light', language: 'ja' }; // デフォルト値
  }
  
  return {
    user: userData,
    preferences: preferences
  };
}

実践的な使用例

API通信での活用

実際のWeb開発では、外部APIとの通信が頻繁に発生します。async/awaitを使うことで、これらの処理を直感的に記述できます。

まず、基本的なHTTP通信の例から見てみましょう。

javascript// fetch API を使った基本的な通信
async function fetchUserInfo(userId) {
  try {
    console.log(`ユーザーID ${userId} の情報を取得中...`);
    
    const response = await fetch(`https://api.example.com/users/${userId}`);
    
    // レスポンスのステータスをチェック
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const userData = await response.json();
    console.log('ユーザー情報取得成功:', userData);
    
    return userData;
  } catch (error) {
    console.error('ユーザー情報の取得に失敗:', error);
    throw error;
  }
}

より実践的な例として、認証が必要なAPIを呼び出す関数を作成してみましょう。

javascript// 認証付きAPI通信の例
class APIClient {
  constructor(baseURL, apiKey) {
    this.baseURL = baseURL;
    this.apiKey = apiKey;
  }
  
  // 共通のHTTPリクエスト処理
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`,
        ...options.headers
      },
      ...options
    };
    
    try {
      const response = await fetch(url, config);
      
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(`API Error: ${response.status} ${errorData.message || response.statusText}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`API request failed for ${endpoint}:`, error);
      throw error;
    }
  }
  
  // ユーザー情報取得
  async getUser(userId) {
    return await this.request(`/users/${userId}`);
  }
  
  // ユーザー情報更新
  async updateUser(userId, userData) {
    return await this.request(`/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(userData)
    });
  }
  
  // 投稿一覧取得
  async getUserPosts(userId, page = 1, limit = 10) {
    return await this.request(`/users/${userId}/posts?page=${page}&limit=${limit}`);
  }
}

APIクライアントを使った実際の処理例を見てみましょう。

javascript// APIクライアントの使用例
async function displayUserDashboard(userId) {
  const api = new APIClient('https://api.example.com', 'your-api-key');
  
  try {
    console.log('ダッシュボード情報を読み込み中...');
    
    // 複数のAPI呼び出しを順次実行
    const user = await api.getUser(userId);
    console.log('✓ ユーザー情報取得完了');
    
    const posts = await api.getUserPosts(userId, 1, 5);
    console.log('✓ 投稿一覧取得完了');
    
    // データを組み合わせてダッシュボード情報を作成
    const dashboardData = {
      user: {
        name: user.name,
        email: user.email,
        joinDate: user.createdAt
      },
      stats: {
        totalPosts: posts.total,
        recentPosts: posts.data.slice(0, 3)
      }
    };
    
    console.log('ダッシュボード情報:', dashboardData);
    return dashboardData;
    
  } catch (error) {
    console.error('ダッシュボード情報の取得に失敗:', error);
    
    // ユーザーに分かりやすいエラーメッセージを返す
    if (error.message.includes('404')) {
      throw new Error('ユーザーが見つかりません');
    } else if (error.message.includes('401')) {
      throw new Error('認証に失敗しました');
    } else {
      throw new Error('サーバーとの通信に失敗しました');
    }
  }
}

// 使用例
displayUserDashboard(123);

複数の非同期処理の制御

実際の開発では、複数の非同期処理を効率的に制御する必要があります。処理を順次実行するか、並列実行するかを適切に選択することが重要です。

以下の図は、順次実行と並列実行の違いを表しています。

mermaidsequenceDiagram
    participant Main as メイン処理
    participant API1 as API1
    participant API2 as API2
    participant API3 as API3
    
    Note over Main, API3: 順次実行(遅い)
    Main ->> API1: リクエスト1
    API1 -->> Main: レスポンス1
    Main ->> API2: リクエスト2
    API2 -->> Main: レスポンス2
    Main ->> API3: リクエスト3
    API3 -->> Main: レスポンス3
    
    Note over Main, API3: 並列実行(速い)
    par
        Main ->> API1: リクエスト1
        API1 -->> Main: レスポンス1
    and
        Main ->> API2: リクエスト2
        API2 -->> Main: レスポンス2
    and
        Main ->> API3: リクエスト3
        API3 -->> Main: レスポンス3
    end

順次実行の例

データに依存関係がある場合は、順次実行を行います。

javascript// 順次実行が必要な処理の例
async function processOrderSequentially(orderId) {
  try {
    // 1. 注文情報を取得
    console.log('注文情報を取得中...');
    const order = await fetchOrder(orderId);
    
    // 2. 在庫を確認(注文情報が必要)
    console.log('在庫を確認中...');
    const inventory = await checkInventory(order.items);
    
    // 3. 決済を処理(注文と在庫情報が必要)
    console.log('決済を処理中...');
    const payment = await processPayment(order, inventory);
    
    // 4. 配送を手配(すべての情報が必要)
    console.log('配送を手配中...');
    const shipping = await arrangeShipping(order, payment);
    
    console.log('注文処理完了:', { order, payment, shipping });
    return { order, payment, shipping };
    
  } catch (error) {
    console.error('注文処理中にエラー:', error);
    throw error;
  }
}

並列実行の例

独立した処理は並列実行で効率化できます。

javascript// 並列実行が可能な処理の例
async function fetchUserDashboardParallel(userId) {
  try {
    console.log('ダッシュボード情報を並列取得中...');
    
    // 複数の独立したAPI呼び出しを並列実行
    const [user, posts, notifications, stats] = await Promise.all([
      fetchUser(userId),
      fetchUserPosts(userId),
      fetchNotifications(userId),
      fetchUserStats(userId)
    ]);
    
    console.log('すべてのデータ取得完了');
    
    return {
      user,
      posts,
      notifications,
      stats
    };
    
  } catch (error) {
    console.error('ダッシュボード情報の取得に失敗:', error);
    throw error;
  }
}

Promise.all、Promise.race の活用

Promise.all の使用

Promise.allは、すべてのPromiseが成功した場合のみ成功し、一つでも失敗すると全体が失敗する特性があります。

javascript// Promise.all の実践的な使用例
async function initializeApplication() {
  try {
    console.log('アプリケーションを初期化中...');
    
    // 必要なリソースをすべて並列で取得
    const [config, userSession, appSettings, featureFlags] = await Promise.all([
      loadConfiguration(),
      validateUserSession(),
      loadAppSettings(),
      loadFeatureFlags()
    ]);
    
    console.log('初期化完了');
    
    return {
      config,
      userSession,
      appSettings,
      featureFlags
    };
    
  } catch (error) {
    console.error('アプリケーションの初期化に失敗:', error);
    throw error;
  }
}

// 個別の初期化関数
async function loadConfiguration() {
  console.log('設定ファイルを読み込み中...');
  return new Promise((resolve) => {
    setTimeout(() => resolve({ apiUrl: 'https://api.example.com' }), 500);
  });
}

async function validateUserSession() {
  console.log('ユーザーセッションを検証中...');
  return new Promise((resolve) => {
    setTimeout(() => resolve({ userId: 123, isValid: true }), 800);
  });
}

async function loadAppSettings() {
  console.log('アプリ設定を読み込み中...');
  return new Promise((resolve) => {
    setTimeout(() => resolve({ theme: 'dark', language: 'ja' }), 300);
  });
}

async function loadFeatureFlags() {
  console.log('機能フラグを読み込み中...');
  return new Promise((resolve) => {
    setTimeout(() => resolve({ newFeature: true, beta: false }), 600);
  });
}

Promise.race の使用

Promise.raceは、最初に完了したPromiseの結果を返します。タイムアウト処理などに有効です。

javascript// Promise.race を使ったタイムアウト処理
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const fetchPromise = fetch(url);
  
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Request timeout after ${timeoutMs}ms`));
    }, timeoutMs);
  });
  
  try {
    const response = await Promise.race([fetchPromise, timeoutPromise]);
    return await response.json();
  } catch (error) {
    if (error.message.includes('timeout')) {
      console.error('リクエストがタイムアウトしました');
    } else {
      console.error('ネットワークエラー:', error);
    }
    throw error;
  }
}

// 使用例
async function testFetchWithTimeout() {
  try {
    const data = await fetchWithTimeout('https://api.example.com/slow-endpoint', 3000);
    console.log('データ取得成功:', data);
  } catch (error) {
    console.log('データ取得失敗:', error.message);
  }
}

複数のAPIから最初に応答があったものを使用する例も見てみましょう。

javascript// 複数のAPIエンドポイントから最速レスポンスを取得
async function fetchFromFastestEndpoint(data) {
  const endpoints = [
    'https://api1.example.com/data',
    'https://api2.example.com/data',
    'https://api3.example.com/data'
  ];
  
  const requests = endpoints.map(url => 
    fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
  );
  
  try {
    const fastestResponse = await Promise.race(requests);
    
    if (!fastestResponse.ok) {
      throw new Error(`HTTP error! status: ${fastestResponse.status}`);
    }
    
    const result = await fastestResponse.json();
    console.log('最速レスポンスを取得:', fastestResponse.url);
    
    return result;
  } catch (error) {
    console.error('すべてのエンドポイントでエラー:', error);
    throw error;
  }
}

まとめ

本記事では、JavaScriptの非同期処理について、基礎から実践まで段階的に解説してきました。

学習した内容の振り返り

技術特徴主な利点主な課題
コールバック関数最も基本的な非同期処理シンプルで理解しやすいコールバック地獄、エラーハンドリングの複雑さ
PromiseES6で導入された改良版チェーン記法、統一的なエラーハンドリング記法がやや複雑
async/awaitES2017で導入された最新版同期処理のような直感的な記法古いブラウザでは未対応

非同期処理の主な活用場面

  1. API通信: サーバーとのデータ交換
  2. ファイル操作: 大きなファイルの読み書き
  3. タイマー処理: 遅延実行や定期実行
  4. ユーザーイベント: ボタンクリックなどの非同期イベント処理

ベストプラクティス

  • 現代の開発ではasync/awaitの使用を推奨
  • エラーハンドリングはtry-catchで統一的に行う
  • 独立した処理はPromise.allで並列実行し、パフォーマンスを向上
  • タイムアウトが必要な場合はPromise.raceを活用
  • コードの可読性と保守性を常に意識する

非同期処理をマスターすることで、よりスムーズで高性能なWebアプリケーションを開発できるようになります。最初は複雑に感じるかもしれませんが、実際のプロジェクトで繰り返し使用することで、自然と身につく技術です。

今回学んだ知識を活用して、ユーザーにとって快適なWebアプリケーションを開発していきましょう。

関連リンク