JavaScript | 非同期処理:async / await – 複数 await の順序

JavaScript JavaScript
スポンサーリンク

複数の await の「順序」を一言でいうと

await を何回も使うとき、
「どこで await を書くか」によって、非同期処理が「順番に」動くか「同時に」動くかが変わります。

同じ await でも、

  • 上から順に 1 個ずつ「直列」に実行する書き方
  • 先に全部スタートさせておいて「並列」に進め、あとでまとめて待つ書き方

の 2 パターンがあります。

ここが重要です。
「とりあえず書いたら、全部直列になっていた」という状態になりがちです。
“本当に順番にやる必要があるのか?” “同時にやって良いのか?” を意識して、
await を置く場所を設計することが、async / await を使う上での大事な一歩です。


基本パターン①:複数 await を“順番に”実行(直列)

コードの上から下に「1つずつ」進むイメージ

まずは一番素直な書き方から。

async function run() {
  console.log("A 開始");
  const a = await taskA();  // A が終わるまで待つ
  console.log("A 完了:", a);

  console.log("B 開始");
  const b = await taskB();  // B が終わるまで待つ
  console.log("B 完了:", b);

  console.log("C 開始");
  const c = await taskC();  // C が終わるまで待つ
  console.log("C 完了:", c);

  console.log("全部完了");
}
JavaScript

この場合の流れはこうです。

  1. taskA() が始まる
  2. await で A が終わるまで待つ
  3. A が終わったら B を開始
  4. B が終わるまで待つ
  5. B が終わったら C を開始
  6. C が終わるまで待つ

つまり、A → B → C の“完全に直列” です。

いつこの書き方が正しいか

この「1 つずつ順番に待つ」パターンが必要になるのは、

  • B は A の結果を使う(A が終わらないと B を始められない)
  • C は B の結果を使う

といった、「前の処理が終わらないと次に進めない」依存関係があるときです。

例えば:

async function run() {
  const user = await fetchUser();          // ユーザー情報が必要
  const posts = await fetchPosts(user.id); // ユーザーIDがないと投稿を取れない
  const comments = await fetchComments(posts[0].id); // 投稿がないとコメントを取れない
}
JavaScript

このような「階段状」の処理では、
すべて順番に await するのが自然です。

ここが重要です。
前後の処理に“依存関係”があるなら、素直に上から順番に await する。
「先にやらないと次が決められない」ものは、無理に並列にしようとしないのが安全です。


基本パターン②:非依存な複数 await を“同時に”走らせる(並列)

先に Promise を「貯めて」から await する

もし A と B が「お互いに関係ない処理」なら、
わざわざ A が終わるのを待ってから B を始める必要はありません。

例えば、こう書いてしまうと完全に直列です。

async function run() {
  const a = await taskA(); // ここで A が終わるまで待つ
  const b = await taskB(); // その後で B を始めて待つ
  console.log(a, b);
}
JavaScript

これを「並列」に動かすには、
先に Promise を作るだけ作って、あとから await します。

async function run() {
  const promiseA = taskA(); // ここで A が動き始める
  const promiseB = taskB(); // ここで B も動き始める

  const a = await promiseA; // A の完了を待つ
  const b = await promiseB; // B の完了を待つ

  console.log(a, b);
}
JavaScript

こうすると、

  • A と B は「ほぼ同時に」スタート
  • それぞれがバックグラウンドで進む
  • await に到達したときに、終わっていなければそこから待つ

という動きになります。

Promise.all を使ってもっと分かりやすく

同じことをもう少し分かりやすく書く方法が Promise.all です。

async function run() {
  const [a, b] = await Promise.all([
    taskA(),
    taskB(),
  ]);

  console.log(a, b);
}
JavaScript

Promise.all は、「配列の中の Promise が全部終わるまで待って、結果をまとめて返す」関数です。

ここが重要です。
複数の非同期処理が「互いに独立」しているなら、
明示的に「並列でやる」と決めてあげると、
全体の待ち時間を短くできます。
そのためのパターンが「Promise を先に作る」か「Promise.all を使う」かのどちらかです。


「順序」と「開始タイミング」は別物だという感覚

「await を書いた場所」で“開始タイミング”が変わる

よくある勘違いは、

「下に await が並んでいると、全部直列になる」

という思い込みです。

実際には、
「Promise をいつ作るか」と「いつ await するか」を分けて考えます。

例えば、次の2つを比べてみてください。

// パターンA:直列
const a = await taskA();
const b = await taskB();

// パターンB:並列
const promiseA = taskA();
const promiseB = taskB();
const a = await promiseA;
const b = await promiseB;
JavaScript

どちらも await 自体は 2 回登場しますが、

  • パターンA → B は「A が終わってから始まる」
  • パターンB → A と B が同時に始まる

という違いがあります。

「順番どおりに結果を使う」ことと「順番どおりに始める」ことは違う

例えば、「A と B を同時に開始して、完了した順に処理する」ことも理論上はできますが、
初心者のうちは、まずは

  • 直列:上から 1 個ずつ await
  • 並列:先に全部 taskX() してから、あとで await or Promise.all

の 2 パターンを使い分けられれば十分です。

ここが重要です。
await の“順序”という言葉で混乱しがちですが、
実際に設計すべきなのは
「いつリクエスト(Promise)を飛ばすか」と
「いつ結果を受け取るか」の 2 つ。
“開始の順番”と“待つ順番”を、少しだけ意識して切り分けてみてください。


具体例でイメージを固める

例1:ユーザー情報と設定を“同時に”取得

ユーザー情報とユーザー設定を API から取ってくる例を考えます。

ユーザー情報:

function fetchUser() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id: 1, name: "Taro" });
    }, 1000); // 1秒かかる
  });
}
JavaScript

設定情報:

function fetchSettings() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ theme: "dark" });
    }, 1000); // 1秒かかる
  });
}
JavaScript

直列で書いた場合

async function runSerial() {
  const user = await fetchUser();       // ここで1秒待つ
  const settings = await fetchSettings(); // さらに1秒待つ

  console.log("直列:", user, settings);
}
JavaScript

合計で約 2 秒かかります。

並列で書いた場合

async function runParallel() {
  const userPromise = fetchUser();      // ここでスタート(非同期)
  const settingsPromise = fetchSettings(); // ここでもスタート

  const user = await userPromise;       // だいたい同じタイミングで終わる
  const settings = await settingsPromise;

  console.log("並列:", user, settings);
}
JavaScript

こちらは、
ほぼ 1 秒ちょっとで両方終わる イメージです。

もちろん、Promise.all を使ってもよいです。

async function runParallelAll() {
  const [user, settings] = await Promise.all([
    fetchUser(),
    fetchSettings(),
  ]);

  console.log("並列(Promise.all):", user, settings);
}
JavaScript

ここが重要です。
「片方が終わるまで、もう片方を始められない理由」がなければ、
基本的には並列パターンを検討する価値があります。
ユーザー情報と設定のように“別々のAPI”は典型的な並列候補です。


例2:前の結果を使う場合は素直に順番に await

今度は、B が A の結果に依存する例です。

async function run() {
  const user = await fetchUser();             // ユーザー情報
  const posts = await fetchPosts(user.id);    // ユーザーIDを使う
  const firstPost = posts[0];
  const comments = await fetchComments(firstPost.id); // 投稿IDを使う

  console.log(user, posts, comments);
}
JavaScript

ここでは、

  • fetchPosts(user.id)user.id がないと始められない
  • fetchComments(firstPost.id)posts がないと決められない

という強い依存があります。

無理に並列にしても意味がありませんし、
バグにつながるだけです。

ここが重要です。
「上の結果を使って次を決める」タイプは、素直に順番に await する。
“並列にすれば速くなる” という発想より、
“依存関係があるかどうか” を優先して考えると、安全かつ分かりやすいコードになります。


エラーと複数 await の組み合わせ

直列の場合のエラー

async function run() {
  try {
    const a = await taskA(); // ここでエラーになったら…
    const b = await taskB(); // ここには来ない
    console.log(a, b);
  } catch (err) {
    console.error("どこかでエラー:", err);
  }
}
JavaScript

A で失敗したら、B はそもそも実行されません。

並列(Promise.all)の場合のエラー

async function run() {
  try {
    const [a, b] = await Promise.all([
      taskA(),
      taskB(), // A か B のどちらかが失敗したら、Promise.all 全体が reject
    ]);
    console.log(a, b);
  } catch (err) {
    console.error("どれか一つでもエラー:", err);
  }
}
JavaScript

Promise.all の場合、

  • A と B のどちらか一つでも失敗 → 全体が失敗
  • 両方とも成功 → [a, b] が返る

という動きになります。

ここが重要です。
「片方が失敗しても、もう片方の結果は欲しい」ような場合は、
Promise.all よりも個別に try / catch する、あるいは Promise.allSettled を使う、など別の設計が必要になります。
“並列にした結果、エラーの扱いがどう変わるか” もセットで考えるのが大事です。


初心者として「複数 await の順序」で本当に押さえてほしいこと

上から順に await を書くと、その部分は基本的に「直列実行」になる。
A の await が終わってからでないと B は始まらない。

依存関係がない複数の非同期処理は、
先に taskX() を全部呼び出して Promise を作っておき、
あとでまとめて await したり Promise.all したりすることで「並列実行」できる。

「いつ“始めるか”」と「いつ“待つか”」は別。
await を書いた場所で“待つタイミング”が決まり、
taskX() を呼んだ瞬間に“処理は開始”している。

前の結果を使う処理(ID を使って次の API を呼ぶ、など)は、素直に順番に await するのが正しい。
無理に並列にしようとしない。

ここが重要です。
複数 await を書くときは、
“これは本当に順番にやる必要があるか?”
“同時に進めて、あとでまとめて待てるのか?”
を一度立ち止まって考えてみてください。
その 1 回の思考の差が、「動くけど遅いコード」と「速くて分かりやすいコード」の分かれ目になります。

もし手を動かして練習したくなったら、こんな課題をやってみてください。

// 1. 1秒後に "A" を返す taskA、1秒後に "B" を返す taskB を作る。
// 2. それぞれを「直列で await する関数」と「並列で await する関数」を書いて、
//    所要時間の違いを console.time などで測ってみる。
// 3. 「どんなときに直列が必要で、どんなときに並列が使えるのか」を
//    自分の言葉でコメントに書いてみる。
JavaScript

「待っている時間」と「実際にやっている仕事」のイメージが結びついてくると、
await の“順序”を自分でコントロールできる感覚がぐっと強くなっていきます。

タイトルとURLをコピーしました