10.

JavaScript 非同期処理完全ガイド (Promise/async/await)

編集
この記事の要点
  • JavaScript の非同期は Callback → Promise → async/await の順に進化
  • Promise: new Promise((resolve, reject) => ...)then/catch/finally で結果を扱う
  • async/await: Promise を同期コードのように書ける糖衣構文。try/catch で例外捕捉
  • 並列実行: Promise.all (全成功) / allSettled (全完了) / race (最初) / any (最初の成功)
  • Event Loop: Microtask (Promise) は Macrotask (setTimeout) より優先される

非同期処理の進化

// 1. Callback (古い)
fs.readFile('a.txt', (err, dataA) => {
  if (err) return console.error(err);
  fs.readFile('b.txt', (err, dataB) => {
    if (err) return console.error(err);
    fs.readFile('c.txt', (err, dataC) => {
      // Callback Hell
    });
  });
});

// 2. Promise (ES6)
fs.promises.readFile('a.txt')
  .then(a => fs.promises.readFile('b.txt'))
  .then(b => fs.promises.readFile('c.txt'))
  .then(c => console.log('done'))
  .catch(err => console.error(err));

// 3. async / await (ES2017)
async function load() {
  try {
    const a = await fs.promises.readFile('a.txt');
    const b = await fs.promises.readFile('b.txt');
    const c = await fs.promises.readFile('c.txt');
  } catch (err) {
    console.error(err);
  }
}

Promise の基本

// 自分で Promise を作る
const sleep = (ms) => new Promise((resolve, reject) => {
  setTimeout(resolve, ms);
});

await sleep(1000);   // 1秒待つ

// 失敗するパターン
const fetchUser = (id) => new Promise((resolve, reject) => {
  if (id < 0) return reject(new Error('invalid id'));
  setTimeout(() => resolve({ id, name: 'taro' }), 100);
});

// then / catch / finally
fetchUser(1)
  .then(user => console.log(user))
  .catch(err  => console.error(err))
  .finally(() => console.log('done'));

// Promise の 3 状態
// pending   → 進行中
// fulfilled → 成功
// rejected  → 失敗

async / await

// async 関数は必ず Promise を返す
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

// 呼び出し側も await が必要
async function main() {
  try {
    const user = await getUser(1);
    console.log(user);
  } catch (err) {
    console.error(err);
  }
}

// Top-level await (ES2022, ESM のみ)
// const user = await getUser(1);   // ESM ファイル直下で OK

// async が返すのは常に Promise
const r = main();
console.log(r instanceof Promise);   // true

並列実行 (Promise.all / allSettled / race / any)

// 全部成功なら resolve。1 つでも失敗で reject
const [a, b, c] = await Promise.all([
  fetch('/api/a'),
  fetch('/api/b'),
  fetch('/api/c'),
]);

// 全部完了 (成否問わず) → 各結果を {status, value/reason} で返す
const results = await Promise.allSettled([
  fetch('/api/a'),
  fetch('/api/b'),   // 失敗してもOK
]);
for (const r of results) {
  if (r.status === 'fulfilled') console.log(r.value);
  else                          console.error(r.reason);
}

// 最初に解決 (成功 / 失敗どちらでも) したものを返す
const fastest = await Promise.race([
  fetch('/api/server1'),
  fetch('/api/server2'),
  new Promise((_, rej) => setTimeout(rej, 3000)),   // タイムアウト
]);

// 最初に成功したものを返す (ES2021)
const ok = await Promise.any([
  fetch('/api/a').then(r => r.ok ? r : Promise.reject()),
  fetch('/api/b').then(r => r.ok ? r : Promise.reject()),
]);

逐次 vs 並列 (パフォーマンス)

// ❌ 逐次 (3 秒かかる)
async function loadSequential() {
  const a = await fetch('/a');   // 1s
  const b = await fetch('/b');   // 1s
  const c = await fetch('/c');   // 1s
  return [a, b, c];
}

// ✅ 並列 (1 秒で済む)
async function loadParallel() {
  const [a, b, c] = await Promise.all([
    fetch('/a'),
    fetch('/b'),
    fetch('/c'),
  ]);
  return [a, b, c];
}

// 依存があるなら逐次でOK
async function depend() {
  const user = await fetch(`/me`).then(r => r.json());
  const posts = await fetch(`/users/${user.id}/posts`);
  return posts;
}

Event Loop と Microtask

console.log('1');

setTimeout(() => console.log('2'), 0);   // Macrotask

Promise.resolve().then(() => console.log('3'));   // Microtask

console.log('4');

// 出力順:
// 1
// 4
// 3   ← Promise (Microtask) は同期処理直後に走る
// 2   ← setTimeout (Macrotask) はその後

// → Microtask Queue は Macrotask 1 回ごとに「全部」消化される
//   Promise.resolve().then(x).then(y).then(z) は全部 2 より先に

エラーハンドリングのベストプラクティス

// ❌ async 関数で reject を放置 → unhandled rejection
async function bad() {
  fetch('/api');   // await 無し → エラー時に握り潰し
}

// ✅ await + try/catch
async function good() {
  try {
    const res = await fetch('/api');
    if (!res.ok) throw new Error(res.statusText);
  } catch (err) {
    console.error(err);
  }
}

// ✅ Promise なら .catch を必ず付ける
fetch('/api')
  .then(r => r.json())
  .catch(err => console.error(err));   // ← 必ず

// 一括 unhandled rejection キャッチ (グローバル)
window.addEventListener('unhandledrejection', (event) => {
  console.error('unhandled:', event.reason);
  event.preventDefault();
});
// Node.js
process.on('unhandledRejection', (err) => { /* ... */ });

setTimeout の最小遅延

// 「0 ミリ秒」でも最低でも 4ms 遅延がブラウザ仕様
setTimeout(() => console.log('!'), 0);
// → 実際には 4ms 以上の遅延

// 即座にやりたいなら queueMicrotask
queueMicrotask(() => console.log('next tick'));

// または Promise.resolve().then
Promise.resolve().then(() => console.log('next tick'));

// Node.js
setImmediate(() => {});   // Macrotask 次
process.nextTick(() => {}); // Microtask より先

Worker / Atomics (真の並列)

// Web Worker (UI スレッドと別の実行コンテキスト)
const w = new Worker('worker.js');
w.postMessage({ task: 'heavy' });
w.onmessage = (e) => console.log(e.data);

// worker.js
self.onmessage = (e) => {
  const result = heavyCompute(e.data);
  self.postMessage(result);
};

// SharedArrayBuffer + Atomics で複数 Worker が共有メモリ操作
const buf = new SharedArrayBuffer(1024);
const view = new Int32Array(buf);
Atomics.add(view, 0, 1);
Atomics.wait(view, 0, 0);
Atomics.notify(view, 0, 1);

Node.js: util.promisify

// 古い callback API を Promise 化
const { promisify } = require('util');
const fs = require('fs');

const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync('a.txt', 'utf8');

// Node 10+ なら fs.promises 標準
const fs2 = require('fs').promises;
const data2 = await fs2.readFile('a.txt', 'utf8');

FAQ

Q: thenawait どちらを使うべき?
A: 基本は async/await (読みやすい)。then はチェーン用、特に Promise を流す中で副作用を挟むときに便利。

Q: for (const x of array) await fn(x) は遅い?
A: 直列実行になるので遅い。並列なら Promise.all(array.map(fn))。順序保証 + 並列度制限したいなら p-limit ライブラリ。

Q: Promise を return しないとエラーは?
A: async 関数内で return await fn()return fn() はどちらも OK。ただし try/catch 内では return await しないと例外捕捉できないので注意。

編集
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 する方法

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