Kodokon kodokon.com

非同期を深く: Promise、async/await、エラー処理

イベントループを活用し、Promise を正しく並列化して、非同期のエラー処理を信頼できるものにしましょう。

11 分 · 3 問

このレッスンを Kodokon で開く

実行モデルは マクロタスク キュー(setTimeout、I/O)と マイクロタスク キュー(.thenawaitqueueMicrotask)に依存しています。各タスクの後、エンジンは次に進む前にマイクロタスクキューを 完全に 空にします。直接の帰結として、解決済みの Promise は常に setTimeout(fn, 0) より先に実行され、あまりに長いマイクロタスクの連鎖は、同期ループと同じくらい確実にレンダリングをフリーズさせ得ます。

JAVASCRIPT
console.log("start");
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
// start, end, microtask, macrotask
マイクロタスクは、保留中のどのマクロタスクよりも先に実行されます。

async/await は Promise の上にかぶせた糖衣構文ですが、性能上の罠を隠しています。ループの内側の各 await は操作を 直列化 します。リクエストが互いに独立しているなら、まずすべてを開始してから Promise.all でまとめて待ちましょう。3つの 100 ミリ秒のリクエストの場合、逐次版は 300 ミリ秒かかりますが、並列版は 100 ミリ秒です。

JAVASCRIPT
const fetchUser = (id) => new Promise((res) => {
  setTimeout(() => res({ id }), 100);
});
async function loadSequential(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // ~100 ms each
  }
  return users;
}
async function loadParallel(ids) {
  return Promise.all(ids.map(fetchUser)); // ~100 ms total
}
同じ結果ですが、レイテンシは呼び出し回数で割られます。

必要な失敗の契約に応じて、適切なコンビネータを選びましょう。Promise.all最初の 失敗で拒否します(全か無か)。allSettled は全員を待ち、それぞれの結末を記述します。race は最初に決着したもの(成功でも失敗でも、タイムアウトに便利)を採ります。any は最初の成功を採り、すべてが失敗した場合にのみ拒否します。部分的な結果を表示しなければならないダッシュボードには、allSettled がほぼ常に正しい選択です。

JAVASCRIPT
const tasks = [
  Promise.resolve("profile loaded"),
  Promise.reject(new Error("network unavailable")),
];
const results = await Promise.allSettled(tasks);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("success:", r.value);
  } else {
    console.error("failure:", r.reason.message);
  }
}
すべての結末が記述され、どの失敗も他を覆い隠しません。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. 同期的な console.logPromise.resolve().then のコールバック、setTimeout(fn, 0) のコールバックは、どの順序で実行されますか?
    • synchronous, setTimeout, then
    • synchronous, then, setTimeout
    • then, synchronous, setTimeout
  2. 互いに独立した10個のリクエストを発行します。合計レイテンシを最小にするのはどの方法ですか?
    • for...of ループの内側でリクエストごとに1つの await
    • すべての Promise を作ってから Promise.all を await する
    • .then の呼び出しを次々に連鎖させる
    • setTimeout を使って呼び出しの間隔を空ける
  3. なぜ await のない try { doAsync(); } catch (e) {}doAsync の拒否を捕捉できないのですか?
    • catch は async 関数の内側では機能しないから
    • Promise が決して await されないため、拒否は try ブロックが抜けた後に起こるから
    • Promise は .catch でしか捕捉できないから