JavaScript | 非同期処理:async / await – 並列 await の書き方

JavaScript JavaScript
スポンサーリンク

並列 await を一言でいうと

「並列 await」は、
“複数の非同期処理を同時にスタートさせて、あとからまとめて結果を受け取る書き方” です。

普通に await を縦に並べると、
A が終わってから BB が終わってから C という「直列実行」になります。

それに対して並列 await では、

  1. 先に全部の Promise を「開始」しておいて
  2. その Promise たちを await(または Promise.all)で「まとめて待つ」

という形にすることで、
全体の待ち時間を短くできる 場面が出てきます。

ここが重要です。
「とりあえず全部 await」と書くのではなく、
“本当に順番でないとダメか? 同時にやってもいい処理じゃないか?” を考えたうえで、意識的に並列 await を使う のがポイントです。


まず「直列 await」との違いを体感する

直列 await の動き(1つずつ順番)

まずは普通の(直列)await から。

async function runSerial() {
  console.log("直列開始");

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

  const b = await taskB(); // そのあと B を実行して待つ
  console.log("B 完了:", b);

  console.log("直列終了");
}
JavaScript

もし taskAtaskB も 1 秒かかる処理なら、
全体でだいたい 2 秒かかります。

この書き方が悪いわけではなく、
「B は A の結果を使うから、A が終わってからでないと始められない」
というケースでは、むしろこれが正解です。

並列 await の動き(同時にスタート)

A と B が「互いに依存していない処理」なら、
先に両方「スタート」させてしまうことができます。

async function runParallel() {
  console.log("並列開始");

  const promiseA = taskA(); // A スタート(待たない)
  const promiseB = taskB(); // B もスタート(待たない)

  const a = await promiseA; // ここで A の完了を待つ
  console.log("A 完了:", a);

  const b = await promiseB; // ここで B の完了を待つ
  console.log("B 完了:", b);

  console.log("並列終了");
}
JavaScript

taskA()taskB() を呼んだ瞬間に、
どちらもバックグラウンドで動き始めます。

もし両方とも 1 秒かかる処理なら、
全体ではだいたい「1 秒ちょっと」で終わります。

ここが重要です。
並列 await の本質は、「await の数」ではなく「Promise をいつ作るか」です。
await を後ろにずらすことで、“開始は一緒、待つのはあと” という形にできます。


基本の並列 await の書き方①:Promise 変数を使う

パターンの形

一番素直な「並列 await」は、この形です。

async function runParallel() {
  const promiseA = taskA();  // ここで A が動き出す
  const promiseB = taskB();  // ここで B も動き出す

  const a = await promiseA;  // A の結果を待つ
  const b = await promiseB;  // B の結果を待つ

  console.log(a, b);
}
JavaScript

ポイントを整理すると、

  1. taskA() / taskB() を呼んだ時点で、「非同期処理のスタート」
  2. その戻り値(Promise)を変数 promiseA, promiseB に保存
  3. 必要なタイミングで await promiseA, await promiseB する

という流れになっています。

具体例:ユーザー情報と設定を同時に取得

よくあるケースで考えてみます。

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

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

これを並列で取ってくる関数:

async function loadUserAndSettings() {
  const userPromise = fetchUser();       // ユーザー取得スタート
  const settingsPromise = fetchSettings(); // 設定取得スタート

  const user = await userPromise;
  const settings = await settingsPromise;

  console.log("ユーザー:", user);
  console.log("設定:", settings);
}
JavaScript

fetchUserfetchSettings はお互いの結果を必要としていません。
だから、同時に始めてしまって問題ないわけです。

ここが重要です。
「Promise を変数に持っておき、あとから await する」は、
並列実行の基本テクニックです。
“スタートタイミング”と“待つタイミング”を分けて考える感覚を、ここで掴んでください。


基本の並列 await の書き方②:Promise.all を使う

Promise.all + await の定番パターン

もう一つ、実務でよく使われるのが Promise.all です。

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

  console.log(a, b);
}
JavaScript

この一行でやっていることは、

  1. taskA()taskB() を同時にスタートさせる
  2. どちらも resolve されるまで待つ
  3. 結果を配列 [aの結果, bの結果] として返す

という動きです。

Promise.all は、
「複数 Promise をまとめて待ち、結果を配列として返す」関数 と覚えておけば大丈夫です。

3つ以上でも同じ

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

  console.log(a, b, c);
}
JavaScript

このときも、A・B・C は同時にスタートします。
全ての Promise が成功したときだけ、
それぞれの結果が [a, b, c] に入ります。

どちらを使うべきか(Promise 変数 vs Promise.all)

どちらも「並列に実行」することに変わりはありません。

違いは主に、

  • Promise 変数方式
    • 各結果を「別々に」待ったり処理したりしたいときに向く
    • 自分で await promiseA; await promiseB; と書く
  • Promise.all 方式
    • 「全部そろってからまとめて使いたい」ときにわかりやすい
    • 結果を同時に受け取りたいときに向く

というイメージです。

ここが重要です。
Promise.all は「全部成功したときだけ一気に使う」感じ。
個別の Promise 変数は「並列スタートはするけど、待つタイミングは自分でコントロール」したいときに強い。
場面に応じて、読みやすい方を選べばOKです。


並列 await を使ってはいけない場面・向いている場面

並列に「してはいけない」場面

次のような場合は、並列 await ではなく直列 await を使うべきです。

例:前の結果が次の処理に必要なとき。

async function run() {
  const user = await fetchUser();             // ユーザーが必要
  const posts = await fetchPosts(user.id);    // user.id がないと取得できない
  const comments = await fetchComments(posts[0].id); // 投稿IDが必要
}
JavaScript

ここで fetchPosts(user.id)fetchComments(posts[0].id) を同時に始めたくても、
posts が無いと posts[0].id が決まらないので、
そもそも並列にしようがありません。

つまり、

  • B が A の結果に依存している
  • C が B の結果に依存している

ような「階段構造」のときは、直列 await が正しい です。

並列に「するべき」場面

逆に、例えば次のようなものは並列向きです。

  • ユーザー情報とお知らせ情報を同時に取得
  • 3つの外部 API から、それぞれ統計情報を取ってくる
  • 複数ファイルを同時に読み込む

これらはお互いの結果を利用せず、
「全部揃えばいい」だけなので、
積極的に並列 await を使ったほうが パフォーマンスが良くなります。

ここが重要です。
「本当に順番にやる必要があるか?」
「連続で書いているだけで、実は関係のない処理じゃないか?」
を一度冷静に見直す癖をつけると、“並列にするチャンス” に気づけるようになります。


並列 await とエラーの扱い(Promise.all の注意点)

Promise.all は「どれか一つでも失敗したら、全体が失敗」

Promise.all には大事な性質があります。

async function run() {
  try {
    const [a, b] = await Promise.all([
      taskA(),
      taskB(),
    ]);
    console.log(a, b);
  } catch (err) {
    console.error("どれか一つが失敗:", err);
  }
}
JavaScript

このとき、

  • taskA が失敗(reject)しても
  • taskB が失敗しても

どちらか一つでも失敗した瞬間に、Promise.all 全体が reject になります。

残りの処理が終わるのを待たずにエラーとして扱われるので、
「片方が失敗しても、もう片方の結果は欲しい」という場合には向きません。

片方だけ失敗しても結果が欲しい場合

例えば、「2つの API に並列で問い合わせて、
片方がダメでも、成功したほうの結果だけでも使いたい」というケースでは、
次のように個別に Promise を await しつつ try / catch する方法があります。

async function run() {
  let a, b;

  try {
    a = await taskA();
  } catch (err) {
    console.error("taskA 失敗:", err);
  }

  try {
    b = await taskB();
  } catch (err) {
    console.error("taskB 失敗:", err);
  }

  console.log("結果:", { a, b });
}
JavaScript

もちろんこの場合も、「並列にスタートさせる」ことはできます。

async function run() {
  const promiseA = taskA();
  const promiseB = taskB();

  let a, b;

  try {
    a = await promiseA;
  } catch (err) { /* ... */ }

  try {
    b = await promiseB;
  } catch (err) { /* ... */ }
}
JavaScript

ここが重要です。
Promise.all は「全部成功してくれないと困る」ときに使う。
「失敗しても、成功した分だけでも欲しい」なら、個別に await+try / catch のほうが柔軟に扱えます。
並列にするかどうかだけでなく、エラーの扱いもセットで設計するのがポイントです。


初心者として「並列 await の書き方」で本当に押さえてほしいこと

並列 await の基本形は 2 つだけです。

  1. 「Promise 変数を作ってから await」する形 const pA = taskA(); const pB = taskB(); const a = await pA; const b = await pB;
  2. 「Promise.all と await」を組み合わせる形 const [a, b] = await Promise.all([taskA(), taskB()]);

どちらも、「処理自体は同時にスタート」しています。
違いは「結果の受け取り方」と「エラーのまとめ方」です。

そして何より大事なのは、

  • 前の結果が必要な処理は、無理に並列にしない(素直に直列 await)
  • お互いに独立している処理は、意識的に並列 await を検討する
  • Promise.all は「全部成功して当たり前」の場面で使う

という判断の軸です。

ここが重要です。
“どこに await を置くか” は、“時間の流れをどうデザインするか” という話です。
「いまは直列だけど、本当は同時に進められるかも?」という視点でコードを見直していくと、
async / await の「設計する楽しさ」が少しずつ見えてきます。

最後に、練習としてこんなことをやってみてください。

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

自分で時間差を「体感」すると、
並列 await の価値と、使いどころの感覚がかなりはっきり見えてきます。

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