11.

JavaScript 同期/非同期処理完全ガイド — Promise/async/await/イベントループ

編集
この記事の要点
  • JavaScript はシングルスレッド + イベントループ。同期処理が長いと UI が固まる
  • 非同期 API: setTimeout / fetch / fs.promises / setInterval
  • コールバック → Promiseasync/await と進化してきた
  • 並列待ち: Promise.all (1 つでも失敗で reject) / Promise.allSettled (全部の結果取得)
  • 真の並列 (CPU 集約処理) は Worker Threads (Node) / Web Workers (ブラウザ)

同期と非同期の違い

観点同期処理非同期処理
実行順上から順に実行、終わるまで次に進まない処理を予約してすぐ次に進む
UI ブロック長いと固まる固まらない
for ループ、JSON.parsefetchsetTimeoutfs.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 なら型エラーで気付けます。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 記述方法
  2. コメント
  3. 変数の宣言
  4. 関数
  5. 演算子
  6. 条件文
  7. 配列
  8. 連想配列
  9. ループ処理
  10. 非同期処理
  11. 同期処理
  12. 確認ウィンドウを表示する方法
  13. 文字の置換
  14. base urlを取得する方法
  15. formのsubmit前にjavascriptを呼び出す方法
  16. undefinedのイコール判定
  17. Javascript のみで form を post で submit する方法

最近更新/作成されたページ