1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

新・JavaScript文法(11):非同期処理とAPI通信

Last updated at Posted at 2025-08-22

前回の記事 では、DOM操作とイベント処理について紹介しました。今回は、非同期処理について、Promise、async/await構文、そしてAPI通信の例を中心に紹介します。

非同期処理とは

非同期処理とは、時間のかかる処理をバックグラウンドで実行しながら、他の処理を続行する仕組みです。WebアプリケーションでAPI通信やファイル読み込みなどを行なう際に有用です。

同期処理と非同期処理の違い

// 同期処理の例(ブロッキング)
console.log('1番目の処理');
console.log('2番目の処理'); // 1番目が完了してから実行
console.log('3番目の処理'); // 2番目が完了してから実行

// 非同期処理の例(ノンブロッキング)
console.log('1番目の処理');
setTimeout(() => {
  console.log('2番目の処理(遅延)');
}, 1000);
console.log('3番目の処理'); // 2番目を待たずに実行

// 実行結果:
// 1番目の処理
// 3番目の処理
// 2番目の処理(遅延)← 1秒後に表示

コールバック地獄(Callback Hell)

従来のコールバック関数による非同期処理は、処理が複雑になると読みにくいコードになってしまいます。

// コールバック地獄の例
function getUser(userId, callback) {
  setTimeout(() => {
    console.log('ユーザー情報を取得中...');
    callback({ id: userId, name: 'Alice' });
  }, 1000);
}

function getUserPosts(userId, callback) {
  setTimeout(() => {
    console.log('投稿一覧を取得中...');
    callback(['投稿1', '投稿2', '投稿3']);
  }, 1000);
}

function getPostComments(postId, callback) {
  setTimeout(() => {
    console.log('コメント一覧を取得中...');
    callback(['コメント1', 'コメント2']);
  }, 1000);
}

// 複数の非同期処理を連続で実行(コールバック地獄)
getUser(1, (user) => {
  console.log('ユーザー:', user);
  getUserPosts(user.id, (posts) => {
    console.log('投稿:', posts);
    getPostComments(posts[0], (comments) => {
      console.log('コメント:', comments);
      // さらに深いネストが続く可能性...
    });
  });
});

Promise(プロミス)

Promiseは、非同期処理をより読みやすく、管理しやすくするために導入された仕組みです。

Promiseの基本概念

// Promiseの基本構造
const promise = new Promise((resolve, reject) => {
  // 非同期処理
  // ここでは何らかの処理を行なったものとして、
  // 成功/失敗の結果を変数に格納している
  const success = true;

  if (success) {
    resolve('成功時の値');    // 成功時にresolveを呼ぶ
  } else {
    reject('失敗時のエラー'); // 失敗時にrejectを呼ぶ
  }
});

// Promiseの利用
promise
  .then((result) => {
    console.log('成功:', result);   // 成功(resolveが呼ばれた)時に実行される
  })
  .catch((error) => {
    console.log('エラー:', error);  // 失敗(rejectが呼ばれた)時に実行される
  })
  .finally(() => {
    console.log('完了(成功・失敗問わず実行)');
  });

Promiseの3つの状態

// 1. Pending(待機中)- 初期状態
const pendingPromise = new Promise((resolve, reject) => {
  // まだ resolve も reject も呼ばれていない状態
});

// 2. Fulfilled(履行済み)- 成功した状態
const fulfilledPromise = Promise.resolve('成功');

// 3. Rejected(拒否済み)- 失敗した状態
const rejectedPromise = Promise.reject('失敗');

console.log(pendingPromise);   // Promise { <pending> }
console.log(fulfilledPromise); // Promise { '成功' }
console.log(rejectedPromise);  // Promise { <rejected> '失敗' }
rejectedPromise.catch(() => {}); // Uncaught (in promise) エラーの場合に catch を使うことでエラーを処理する

Promiseチェーン

先ほどのコールバック地獄のコードをPromiseで書き換えると、以下のようになります。

function getUserPromise(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('ユーザー情報を取得中...');
      resolve({ id: userId, name: 'Alice' });
    }, 1000);
  });
}

function getUserPostsPromise(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('投稿一覧を取得中...');
      resolve(['投稿1', '投稿2', '投稿3']);
    }, 1000);
  });
}

function getPostCommentsPromise(postId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('コメント一覧を取得中...');
      resolve(['コメント1', 'コメント2']);
    }, 1000);
  });
}

// Promiseチェーンで順次実行
getUserPromise(1)
  .then((user) => {
    console.log('ユーザー:', user);
    return getUserPostsPromise(user.id);
  })
  .then((posts) => {
    console.log('投稿:', posts);
    return getPostCommentsPromise(posts[0]);
  })
  .then((comments) => {
    console.log('コメント:', comments);
  })
  .catch((error) => {
    console.error('エラーが発生しました:', error);
  });

Promise.allとPromise.race

先ほどまでのコードは順次実行でしたが、複数のPromiseを並行しての実行もできます。

// 複数のPromiseを並行実行
const promise1 = Promise.resolve('結果1');
const promise2 = Promise.resolve('結果2');
const promise3 = Promise.resolve('結果3');

// Promise.all - すべてのPromiseが完了するまで待つ
Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log('すべて完了:', results); // ['結果1', '結果2', '結果3']
  })
  .catch((error) => {
    console.log('いずれかが失敗:', error);
  });

// Promise.race - 最初に完了したPromiseの結果を返す
Promise.race([promise1, promise2, promise3])
  .then((result) => {
    console.log('最初に完了:', result); // 最初に完了したPromiseの結果
  });

// 実用例:タイムアウト付きのAPIリクエスト
function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('タイムアウト')), timeout);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

async/await構文

async/await構文は、Promiseをより同期処理のように書けるようにするためのものです。

async/awaitの基本

async をつけた関数は常にPromiseを返すため、手動で resolve()reject() を呼ぶ必要がありません。

また、await キーワードを使うことで、Promiseの完了を待つことができます。たとえば、以下のコードの場合、getUserPostsPromise()getUserPromise() が完了するまで実行されません。

// Promiseチェーンで書いた処理をasync/awaitで書き直し
async function processUserData() {
  try {
    // awaitで非同期処理を待つ
    const user = await getUserPromise(1);
    console.log('ユーザー:', user);

    const posts = await getUserPostsPromise(user.id);
    console.log('投稿:', posts);

    const comments = await getPostCommentsPromise(posts[0]);
    console.log('コメント:', comments);

    return comments; // async関数は常にPromiseを返す
  } catch (error) {
    console.error('エラーが発生しました:', error);
  }
}

// async関数の呼び出し
processUserData()
  .then((result) => {
    console.log('最終結果:', result);
  });

async関数の特徴

// async関数は常にPromiseを返す
async function getValue() {
  return 'Hello World'; // 自動的にPromise.resolve('Hello World')になる
}

getValue().then(console.log); // "Hello World"

// エラーを投げるとrejectedなPromiseを返す
async function getError() {
  throw new Error('エラーが発生しました');  // 自動的にPromise.reject(new Error('エラーが発生しました'))になる
}

getError().catch(console.error); // Error: エラーが発生しました

// awaitは基本的にasync関数内でのみ使用可能
async function example() {
  const result = await Promise.resolve('成功');
  console.log(result); // "成功"
}

fetch APIを使った例

fetch APIは、HTTP通信を行なうためのAPIです。async/awaitと組み合わせることで、わかりやすいコードが書けます。

基本的なfetch APIの使用

以下のコードでは、サンプルデータを提供する JSON Placeholder から架空のユーザー情報を取得しています。

// GET リクエスト
async function fetchUsers() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');

    // レスポンスステータスの確認
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }

    const users = await response.json();
    console.log('ユーザー一覧:', users);
    return users;
  } catch (error) {
    console.error('ユーザー取得エラー:', error);
    throw error; // 呼び出し元にエラーを伝播
  }
}

// POST リクエスト
async function createUser(userData) {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData)
    });

    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }

    const newUser = await response.json();
    console.log('新しいユーザー:', newUser);
    return newUser;
  } catch (error) {
    console.error('ユーザー作成エラー:', error);
    throw error;
  }
}

// 使用例
fetchUsers();
createUser({ name: 'John Doe', email: 'john@example.com' });

実用的なAPI通信クラス

以下は、CRUD操作を行なうためのAPI通信クラスの例です。

// API通信を管理するクラス
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    };

    try {
      const response = await fetch(url, config);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      // レスポンスの Content-Type を確認
      const contentType = response.headers.get('content-type');
      if (contentType && contentType.includes('application/json')) {
        return await response.json();
      } else {
        return await response.text();
      }
    } catch (error) {
      console.error(`API Request Error (${endpoint}):`, error);
      throw error;
    }
  }

  async get(endpoint) {
    return this.request(endpoint);
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, {
      method: 'DELETE',
    });
  }
}

// 使用例
const api = new ApiClient('https://jsonplaceholder.typicode.com');

async function userOperations() {
  try {
    // ユーザー一覧を取得
    const users = await api.get('/users');
    console.log('ユーザー一覧:', users.slice(0, 3)); // 最初の3件

    // 特定のユーザーを取得
    const user = await api.get('/users/1');
    console.log('ユーザー詳細:', user);

    // 新しいユーザーを作成
    const newUser = await api.post('/users', {
      name: 'Test User',
      email: 'test@example.com',
    });
    console.log('作成されたユーザー:', newUser);

  } catch (error) {
    console.error('操作中にエラーが発生しました:', error);
  }
}

userOperations();

エラーハンドリングの例

// エラーハンドリングの例
class UserService {
  constructor() {
    this.api = new ApiClient('https://jsonplaceholder.typicode.com');
  }

  async getUser(userId) {
    try {
      const user = await this.api.get(`/users/${userId}`);
      return { success: true, data: user };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        type: this.getErrorType(error)
      };
    }
  }

  async getUserWithRetry(userId, maxRetries = 3) {
    let attempt = 1;

    while (attempt <= maxRetries) {
      try {
        const user = await this.api.get(`/users/${userId}`);
        return { success: true, data: user };
      } catch (error) {
        console.warn(`取得失敗 (試行 ${attempt}/${maxRetries}):`, error.message);

        if (attempt === maxRetries) {
          return {
            success: false,
            error: `${maxRetries}回試行しましたが失敗しました: ${error.message}`,
            type: 'retry_exhausted'
          };
        }

        // 指数バックオフで待機
        await this.delay(Math.pow(2, attempt) * 1000);
        attempt++;
      }
    }
  }

  getErrorType(error) {
    if (error.message.includes('404')) return 'not_found';
    if (error.message.includes('500')) return 'server_error';
    if (error.message.includes('Failed to fetch')) return 'network_error';
    return 'unknown_error';
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// 使用例
const userService = new UserService();

async function demonstrateErrorHandling() {
  // 正常なケース
  const result1 = await userService.getUser(1);
  if (result1.success) {
    console.log('ユーザー取得成功:', result1.data.name);
  } else {
    console.error('エラー:', result1.error);
  }

  // 存在しないユーザー(404エラー)
  const result2 = await userService.getUser(999);
  if (!result2.success) {
    console.error('エラータイプ:', result2.type); // "not_found"
  }

  // リトライ機能付きでの取得
  const result3 = await userService.getUserWithRetry(1);
  console.log('リトライ結果:', result3.success ? '成功' : '失敗');
}

demonstrateErrorHandling();

発展:イベントループの仕組み

JavaScriptの非同期処理を理解するうえで、イベントループの仕組みを簡単に紹介します。

// イベントループの動作例
console.log('1: 同期処理');

setTimeout(() => {
  console.log('2: マクロタスク(Timer)');
}, 0);

Promise.resolve().then(() => {
  console.log('3: マイクロタスク(Promise)');
});

console.log('4: 同期処理');

// 実行順序:
// 1: 同期処理
// 4: 同期処理
// 3: マイクロタスク(Promise)
// 2: マクロタスク(Timer)

// 理由:
// 1. 同期処理が最優先で実行される
// 2. マイクロタスク(Promise)がマクロタスク(Timer)より優先される

復習

問題

1. 以下のコードの実行順序を予想する

console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

console.log('D');

2. fetch APIを使って、指定されたURLからデータを取得し、エラーハンドリングも含めたasync関数を作成する

たとえば、このasync関数を fetchData(url) とし、https://jsonplaceholder.typicode.com/posts/1 からデータを取得する場合、以下のように使用します。

fetchData('https://jsonplaceholder.typicode.com/posts/1')
  .then(post => console.log('投稿タイトル:', post.title))
  .catch(error => console.error('処理に失敗しました:', error.message));

3. 次のコールバック関数を使った処理を、async/awaitを使って書き換える

function fetchUserData(userId, callback) {
  setTimeout(() => {
    if (userId > 0) {
      callback(null, { id: userId, name: 'User ' + userId });
    } else {
      callback(new Error('Invalid user ID'), null);
    }
  }, 1000);
}

// 使用例
fetchUserData(1, (error, user) => {
  if (error) {
    console.error('エラー:', error.message);
  } else {
    console.log('ユーザー:', user);
  }
});

解答例

1. 実行順序

// 実行順序: A → D → C → B
// 理由:
// 1. 同期処理(A, D)が最初に実行される
// 2. マイクロタスク(Promise.then)がマクロタスク(setTimeout)より優先される

2. コールバックからasync/awaitへの書き換え

// Promise版の関数を作成
function fetchUserDataPromise(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: 'User ' + userId });
      } else {
        reject(new Error('Invalid user ID'));
      }
    }, 1000);
  });
}

// async/awaitを使った書き換え
async function getUserData(userId) {
  try {
    console.log(`ユーザー${userId}のデータを取得中...`);
    const user = await fetchUserDataPromise(userId);
    console.log('ユーザー:', user);
    return user;
  } catch (error) {
    // エラーは呼び出し元で処理するため、ここではスローするだけにする
    throw error;
  }
}

// 使用例
async function example() {
  await getUserData(1);  // 正常なケース
  await getUserData(-1); // エラーケース
}

example().catch(console.error);

3. fetch APIを使ったデータ取得関数

async function fetchData(url) {
  try {
    console.log(`データ取得開始: ${url}`);

    const response = await fetch(url);

    // HTTPステータスの確認
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
    }

    // Content-Typeに応じてパース方法を変更
    const contentType = response.headers.get('content-type');
    let data;

    if (contentType && contentType.includes('application/json')) {
      data = await response.json();
    } else {
      data = await response.text();
    }

    console.log('データ取得成功:', data);
    return data;

  } catch (error) {
    console.error('データ取得エラー:', error.message);

    // エラーの種類に応じた処理
    if (error.name === 'TypeError') {
      console.error('ネットワークエラーまたはCORSエラーの可能性があります');
    }

    throw error; // 呼び出し元にエラーを伝播
  }
}

// 使用例
fetchData('https://jsonplaceholder.typicode.com/posts/1')
  .then(post => console.log('投稿タイトル:', post.title))
  .catch(error => console.error('処理に失敗しました:', error.message));

まとめ

非同期処理について、Promise、async/await構文、そして実践的なAPI通信の方法を紹介しました。

技術的メリット

  1. コードの可読性向上
    • コールバック地獄の解消
    • エラーハンドリングの明確化
    • async/awaitにより同期処理のような書き方が可能
  2. パフォーマンスの向上
    • 並行処理による実行時間の短縮
    • ノンブロッキング処理によるユーザー体験の向上
    • Promise.all による複数処理
  3. エラー処理の改善
    • try/catch文による統一的なエラーハンドリング
    • Promise.catch によるエラー処理の一元化
    • リトライ機能の実装が容易
  4. 実用性の高い機能
    • fetch APIによるHTTP通信
    • RESTful APIとの連携
    • 実際のWebアプリケーション開発で即活用可能

活用場面の例

  • API通信
    REST API、GraphQL、WebSocket通信
  • ファイル操作
    画像・動画のアップロード、ダウンロード
  • データベースアクセス
    クエリ実行、トランザクション処理
  • ユーザーインターフェイス
    ローディング状態の管理、プログレス表示

注意すべきポイント

非同期処理は便利な反面、以下の点に注意が必要です。

  • エラーハンドリング:非同期処理にはエラー処理を実装
  • メモリリークの防止:不要なPromiseや非同期処理のクリーンアップ
  • パフォーマンスの考慮:過度な並行処理によるリソース消費への注意
  • デバッグの困難さ:非同期処理特有のデバッグ手法の習得

次回 は、最新のJavaScript機能と開発の今後の展望について紹介します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?