非同期プログラミング - コールバック、Promise、async/await

15分 で読める | 2025.12.23

公式ドキュメント

非同期処理とは

非同期処理は、時間のかかる操作(ファイル読み込み、API呼び出し、タイマー等)を待たずに、他の処理を続行できる仕組みです。

flowchart LR
 subgraph Sync["同期処理(順番に待つ)"]
 S1["タスク1"] --> S1C["完了"] --> S2["タスク2"] --> S2C["完了"] --> S3["タスク3"] --> S3C["完了"]
 end
flowchart TB
 subgraph Async["非同期処理(並行して実行)"]
 A1["タスク1 開始"]
 A2["タスク2 開始"]
 A3["タスク3 開始"]
 A1C["タスク1 完了"]
 A2C["タスク2 完了"]
 A3C["タスク3 完了"]
 end

イベントループ

JavaScriptはシングルスレッドですが、イベントループにより非同期処理を実現します。

flowchart TB
 CallStack["Call Stack<br/>(同期的なコードを実行)"]
 EventLoop["Event Loop<br/>(コールスタックが空になったらキューから取り出す)"]
 Microtask["Microtask Queue<br/>(Promise, queueMicrotask)<br/>★ 優先度: 高"]
 Macrotask["Macrotask Queue<br/>(setTimeout, I/O)<br/>優先度: 低"]

 CallStack <--> EventLoop
 EventLoop --> Microtask
 EventLoop --> Macrotask

実行順序の例

console.log('1'); // 同期

setTimeout(() => console.log('2'), 0); // マクロタスク

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

console.log('4'); // 同期

// 出力: 1, 4, 3, 2

コールバック

最も基本的な非同期パターンです。

// コールバック地獄の例
fs.readFile('file1.txt', (err, data1) => {
 if (err) throw err;
 fs.readFile('file2.txt', (err, data2) => {
 if (err) throw err;
 fs.readFile('file3.txt', (err, data3) => {
 if (err) throw err;
 console.log(data1, data2, data3);
 });
 });
});

問題点:

  • ネストが深くなる(コールバック地獄)
  • エラーハンドリングが複雑
  • 可読性が低い

Promise

Promiseは、非同期操作の最終的な完了(または失敗)を表すオブジェクトです。

// Promiseの作成
const myPromise = new Promise((resolve, reject) => {
 setTimeout(() => {
 const success = true;
 if (success) {
 resolve('成功しました!');
 } else {
 reject(new Error('失敗しました'));
 }
 }, 1000);
});

// Promiseの使用
myPromise
 .then(result => console.log(result))
 .catch(error => console.error(error))
 .finally(() => console.log('完了'));

Promiseチェーン

fetchUser(userId)
 .then(user => fetchPosts(user.id))
 .then(posts => fetchComments(posts[0].id))
 .then(comments => {
 console.log(comments);
 })
 .catch(error => {
 console.error('Error:', error);
 });

Promise.all / Promise.race

// Promise.all: すべて完了を待つ
const results = await Promise.all([
 fetchUser(1),
 fetchUser(2),
 fetchUser(3)
]);
// → [user1, user2, user3]

// Promise.race: 最初に完了したものを返す
const fastest = await Promise.race([
 fetchFromServer1(),
 fetchFromServer2()
]);

// Promise.allSettled: すべての結果を取得(失敗含む)
const results = await Promise.allSettled([
 fetchUser(1),
 fetchUser(999) // 存在しないユーザー
]);
// → [{status: 'fulfilled', value: user1}, {status: 'rejected', reason: Error}]

async/await

async/awaitはPromiseをより直感的に書くための構文糖です。

// async関数
async function fetchUserData(userId) {
 try {
 const user = await fetchUser(userId);
 const posts = await fetchPosts(user.id);
 const comments = await fetchComments(posts[0].id);
 return { user, posts, comments };
 } catch (error) {
 console.error('Error:', error);
 throw error;
 }
}

ポイント: 互いに依存しない複数の非同期処理はPromise.allで並列実行することで、合計実行時間を大幅に短縮できます。

並列実行

// 順次実行(遅い)
async function sequential() {
 const user1 = await fetchUser(1); // 1秒
 const user2 = await fetchUser(2); // 1秒
 const user3 = await fetchUser(3); // 1秒
 // 合計: 3秒
}

// 並列実行(速い)
async function parallel() {
 const [user1, user2, user3] = await Promise.all([
 fetchUser(1),
 fetchUser(2),
 fetchUser(3)
 ]);
 // 合計: 約1秒
}

エラーハンドリング

try/catch

async function fetchData() {
 try {
 const data = await riskyOperation();
 return data;
 } catch (error) {
 if (error.code === 'NOT_FOUND') {
 return null;
 }
 throw error; // 再スロー
 }
}

エラーラッパー

// エラーを配列で返すパターン
async function safeAsync(promise) {
 try {
 const data = await promise;
 return [null, data];
 } catch (error) {
 return [error, null];
 }
}

// 使用
const [error, user] = await safeAsync(fetchUser(id));
if (error) {
 console.error('Failed to fetch user:', error);
 return;
}
console.log(user);

実践メモ: 並行(Concurrent)と並列(Parallel)は異なる概念です。JavaScriptのイベントループは並行処理であり、真の並列処理にはWorker Threadsが必要です。

並行と並列

flowchart LR
 subgraph Concurrent["並行(Concurrent)"]
 direction LR
 Note1["複数のタスクが時間的に重なって実行<br/>(シングルスレッドでも実現可能)"]
 end

 subgraph Parallel["並列(Parallel)"]
 direction LR
 Note2["複数のタスクが同時に実行<br/>(マルチスレッド/マルチコアが必要)"]
 end

Node.jsでの並列処理

// Worker Threads
const { Worker } = require('worker_threads');

function runCPUIntensiveTask(data) {
 return new Promise((resolve, reject) => {
 const worker = new Worker('./heavy-task.js', {
 workerData: data
 });
 worker.on('message', resolve);
 worker.on('error', reject);
 });
}

非同期イテレーション

// for await...of
async function* generateUsers() {
 for (let id = 1; id <= 3; id++) {
 yield await fetchUser(id);
 }
}

for await (const user of generateUsers()) {
 console.log(user);
}

// AsyncIterator
const stream = fs.createReadStream('large-file.txt');
for await (const chunk of stream) {
 console.log(chunk);
}

注意: 以下のアンチパターンはパフォーマンス低下やバグの原因になります。特に独立した処理の順次実行は見落としやすいので注意しましょう。

アンチパターン

await の乱用

// 悪い例: 不要なawait
async function bad() {
 return await fetchData(); // awaitは不要
}

// 良い例
async function good() {
 return fetchData(); // Promiseをそのまま返す
}

順次実行の罠

// 悪い例: 独立した処理を順次実行
const user = await fetchUser(id);
const config = await fetchConfig(); // userに依存していない

// 良い例: 並列実行
const [user, config] = await Promise.all([
 fetchUser(id),
 fetchConfig()
]);

まとめ

非同期プログラミングは、モダンなJavaScript開発に不可欠です。コールバックからPromise、async/awaitへと進化し、より読みやすく保守しやすいコードが書けるようになりました。イベントループの仕組みを理解し、適切なエラーハンドリングと並列実行を行うことで、効率的な非同期処理を実現できます

参考リソース

← 一覧に戻る
PR
PR
PR
PR