タイトル: 同期処理
SEOタイトル: JavaScript 同期/非同期処理完全ガイド — Promise/async/await/イベントループ
| この記事の要点 |
|
同期と非同期の違い
| 観点 | 同期処理 | 非同期処理 |
|---|---|---|
| 実行順 | 上から順に実行、終わるまで次に進まない | 処理を予約してすぐ次に進む |
| UI ブロック | 長いと固まる | 固まらない |
| 例 | for ループ、JSON.parse | fetch、setTimeout、fs.readFile |
| 結果取得 | 直接 return | コールバック / Promise / await |
イベントループの基本
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 出力順: 1 → 4 → 3 → 2
// 理由:
// 1, 4 は同期 (Call Stack で即実行)
// 3 は microtask (Promise) → Call Stack 空になった瞬間
// 2 は macrotask (Timer) → microtask 後
優先度: 同期コード > microtask (Promise / queueMicrotask) > macrotask (setTimeout / setInterval / I/O)。
コールバック地獄
// ❌ Callback Hell
getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getItems(orders[0].id, (err, items) => {
if (err) return handleError(err);
getPrice(items[0].id, (err, price) => {
if (err) return handleError(err);
// 深くなりすぎ
console.log(price);
});
});
});
});
Promise: 非同期処理の合成
// 基本: Promise の作成
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve(42), 1000);
});
// 消費: then / catch / finally
p.then(value => console.log(value)) // 42
.catch(err => console.error(err))
.finally(() => console.log('done'));
// チェーンで非同期を直列に
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(res => res.json())
.then(orders => console.log(orders))
.catch(err => console.error(err));
async/await: Promise の糖衣構文
// ✅ async/await で同期風に書ける
async function loadUserData(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const items = await getItems(orders[0].id);
const price = await getPrice(items[0].id);
return price;
} catch (err) {
console.error(err);
}
}
// 呼び出し側
loadUserData(123).then(price => console.log(price));
// または別の async 関数内で
const price = await loadUserData(123);
並列待ち: Promise.all / allSettled / race / any
// Promise.all: 全部成功なら配列、1つでも失敗で reject
const [users, orders, items] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/orders').then(r => r.json()),
fetch('/items').then(r => r.json()),
]);
// Promise.allSettled: 全部の結果を待つ (失敗もOK)
const results = await Promise.allSettled([
fetch('/a'), fetch('/b'), fetch('/c'),
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});
// Promise.race: 最初に決着した1つ
const winner = await Promise.race([
fetch('/slow'),
new Promise((_, rej) => setTimeout(() => rej('timeout'), 5000)),
]);
// Promise.any: 最初に成功した1つ (ES2021)
const fastest = await Promise.any([
fetch('//cdn1/data.json'),
fetch('//cdn2/data.json'),
fetch('//cdn3/data.json'),
]);
逐次 vs 並列の落とし穴
// ❌ 直列実行 (合計 3 秒)
const a = await fetchA(); // 1秒
const b = await fetchB(); // 1秒
const c = await fetchC(); // 1秒
// ✅ 並列実行 (合計 1 秒)
const [a, b, c] = await Promise.all([
fetchA(), fetchB(), fetchC(),
]);
// ❌ map + await でも直列にはならない
const results = await Promise.all(
urls.map(url => fetch(url))
);
setTimeout / setInterval
// 1 秒後に実行
const timerId = setTimeout(() => {
console.log('1 second later');
}, 1000);
// キャンセル
clearTimeout(timerId);
// 1 秒ごとに繰り返し
const intervalId = setInterval(() => {
console.log('tick');
}, 1000);
// 停止
clearInterval(intervalId);
// 注意: setTimeout(fn, 0) でも即時実行されない
// 最低でも 4ms (HTML5 仕様で網入れあり) かかる
microtask: process.nextTick / queueMicrotask
// queueMicrotask (標準。ブラウザ + Node)
queueMicrotask(() => {
console.log('microtask');
});
// process.nextTick (Node のみ。microtask より更に優先)
process.nextTick(() => {
console.log('nextTick');
});
// 実行優先度
// 同期 > process.nextTick > microtask (Promise) > macrotask (setTimeout)
エラーハンドリング
// async 関数の中は try/catch で受ける
async function loadData() {
try {
const data = await fetch('/api/data').then(r => r.json());
return data;
} catch (err) {
console.error('Failed:', err);
throw err; // 上に伝播
}
}
// Promise チェーンは .catch で
fetch('/api/data')
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.error(err));
// 未捕捉 Promise を補足
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled:', reason);
});
// ブラウザ
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled:', e.reason);
});
真の並列: Worker Threads / Web Workers
JavaScript はシングルスレッドだが、別スレッドで CPU 集約処理を回すことができます:
// Node.js Worker Threads
import { Worker } from 'node:worker_threads';
const worker = new Worker('./heavy.js');
worker.on('message', (result) => console.log(result));
worker.postMessage({ task: 'compute', data: largeArray });
// ブラウザ Web Workers
const worker = new Worker('worker.js');
worker.onmessage = (e) => console.log(e.data);
worker.postMessage({ task: 'compute' });
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
ベストプラクティス
- async/await を基本。Promise チェーンより読みやすい
- 並列できるものは Promise.all でまとめる
- エラーは必ず try/catch または .catch() で受ける
- 長い CPU 処理はWorker に逃がし、UI を固まらせない
- process.exit() は最終手段。awaitが終わる前に切ると非同期処理が失われる
- テストでは jest.useFakeTimers() / vi.useFakeTimers() で時間を操る
FAQ
Q: forEach の中で await が効かないのはなぜ?
A: forEach は async 関数を受け取っても待たない設計。for...of または Promise.all + map を使います。
Q: コールバック関数を Promise 化したい
A: Node なら util.promisify(fn)。自前なら new Promise((resolve, reject) => fn(args, (err, val) => err ? reject(err) : resolve(val)))。
Q: await を忘れたら何が起きる?
A: その関数はPromise オブジェクトをそのまま返します。中身を使うとバグります。TypeScript なら型エラーで気付けます。