この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:5
ページ更新者:guest
更新日時:2026-06-11 07:29:05

タイトル: 同期処理
SEOタイトル: 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 なら型エラーで気付けます。