JavaScript / LESSON 15 / JS15

JavaScript:非同期処理(Promise / async-await)

1学習の目的

2基礎解説

やりたいこと書き方備考
結果を待つconst data = await f();async 関数の中でのみ
待てる関数を作るasync function f() { }戻り値は必ず Promise
失敗を受け止めるtry { await f(); } catch (e) { }同期処理と同じ書き方
まとめて並列実行await Promise.all([a(), b()])待ち時間が短くなる
✅ 覚えるべき重要ポイント
  1. await を忘れると Promise そのものが入る。表示すると [object Promise] になる。「なぜかデータが取れない」の原因はほぼこれ。
  2. awaitasync を付けた関数の中でしか使えない。非同期を呼ぶ側も async にする必要がある。
  3. async 関数の戻り値は必ず Promisereturn 42 と書いても、受け取る側は await が要る。
  4. エラーは try / catch で受け止める(JS10と同じ)。await を付けないと catch できない点に注意。
  5. 順番に待つ必要がないなら Promise.all。3秒の処理を3つ直列に待つと9秒だが、並列なら3秒で済む。

現場使用例:APIからのデータ取得、ファイルの読み込み、画像の遅延読み込み、一定時間待ってから再試行。「すぐには終わらない処理」はすべて非同期になる。

// 指定ミリ秒待つ関数(Promiseを返す)
const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

// async 関数の中でだけ await が使える
async function main() {
  const a = await wait(100, "データA");
  console.log(a);                     // データA

  // await を忘れると Promise が入ってしまう
  const b = wait(100, "データB");
  console.log(b);                     // Promise { <pending> }

  // 失敗は try/catch で受け止める
  try {
    await Promise.reject(new Error("通信失敗"));
  } catch (e) {
    console.log(`エラー:${e.message}`);
  }

  // 順番に待つ必要がなければ並列に
  const [x, y] = await Promise.all([wait(100, "X"), wait(100, "Y")]);
  console.log(x, y);                  // X Y(合計100msで完了)
}

main();

3基本ドリル(10問)

JS15-D01 値 42 を返す async 関数 getValue を定義し、await で受け取って出力せよ(呼び出し側も async 関数にすること)。 コーディング★☆☆無料
期待される結果

出力 == 42

ヒント

async function getValue() { return 42; } を作り、async function main() の中で await getValue() する。最後に main() を呼ぶ。

模範解答
async function getValue() {
  return 42;
}

async function main() {
  const value = await getValue();
  console.log(value);
}

main();
解説

async を付けた関数の戻り値は必ず Promise になる。だから 42 をそのまま返しても、受け取る側は await が必要。

JS15-D02 const data = getData();await を付けずに書いたとき、data に入るのはどれか。
A. 結果の値 B. Promise オブジェクト C. undefined D. エラー
選択★☆☆無料
期待される結果

解答 == B

ヒント

await は「結果が出るまで待って取り出す」役割。

模範解答
B
解説

表示すると [object Promise]Promise { <pending> } になる。「なぜかデータが表示されない」の原因はほぼこれ

JS15-D03 50ミリ秒待ってから "完了" を返す関数 wait を作り、await で受け取って出力せよ。 コーディング★☆☆無料
期待される結果

出力 == 完了

ヒント

new Promise((resolve) => setTimeout(() => resolve("完了"), 50)) を返す関数を作る。

模範解答
const wait = () =>
  new Promise((resolve) => setTimeout(() => resolve("完了"), 50));

async function main() {
  console.log(await wait());
}

main();
解説

resolve を呼んだ時点で「成功」が確定し、await していた場所に値が渡る。これがPromiseの基本の仕組み。

JS15-D04 非同期の結果を待つコードを完成させよ。 穴埋め★☆☆無料
コード
const getName = async () => "田中";

async function main() {
  const name = ____ getName();
  console.log(name);
}

main();
期待される結果

出力 == 田中

ヒント

「待つ」という意味の英単語5文字。

模範解答
await
解説

これを書かないと Promise { '田中' } のような表示になる。非同期の関数を呼んだら awaitと覚える。

JS15-D05 await が使えるのはどこか。
A. どこでも B. async を付けた関数の中 C. try の中だけ D. ループの中だけ
選択★☆☆無料
期待される結果

解答 == B

ヒント

await と async は必ずセットで使う。

模範解答
B
解説

async の無い関数で await を書くと SyntaxError になる。非同期を扱う関数は、呼び出す側も連鎖的に async にしていく必要がある。

JS15-D06 必ず失敗する非同期処理を try / catch で受け止め、「エラー:通信失敗」と出力せよ。 コーディング★★☆無料
期待される結果

出力 == エラー:通信失敗

ヒント

Promise.reject(new Error("通信失敗")) を await し、catch (e) で e.message を出す。

模範解答
async function main() {
  try {
    await Promise.reject(new Error("通信失敗"));
  } catch (e) {
    console.log(`エラー:${e.message}`);
  }
}

main();
解説

同期処理とまったく同じ書き方でエラーを扱えるのが async / await の最大の利点。ただし await を付け忘れると catch できないので注意。

JS15-D07 2つの非同期処理を並列に実行するコードを完成させよ。 穴埋め★★☆無料
コード
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

async function main() {
  const [a, b] = await Promise.____([wait(20, "A"), wait(20, "B")]);
  console.log(a, b);
}

main();
期待される結果

出力 == A B

ヒント

「すべて」を意味する英単語3文字。

模範解答
all
解説

直列に await すると待ち時間が足し算になるが、Promise.all なら最も遅いものの時間で済む。20ms × 2 が 20ms になる。

JS15-D08 3秒かかる処理を3つ実行する。最も速く終わる書き方はどれか。
A. 順に await する B. Promise.all でまとめる C. for ループで await D. どれも同じ
選択★★☆無料
期待される結果

解答 == B

ヒント

互いに依存しない処理は同時に走らせられる。

模範解答
B
解説

AとCは合計9秒かかるが、Bなら3秒で終わる。ただし「前の結果を次で使う」場合は並列にできないので、順に await するしかない。

JS15-D09 wait(30, "A") と wait(10, "B") を順番に await し、「A → B」の形式で出力せよ(wait は指定ミリ秒後に値を返す関数)。 コーディング★★☆無料
期待される結果

出力 == A → B

ヒント

const a = await wait(...) を2回書く。先に書いたほうが先に完了する。

模範解答
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

async function main() {
  const a = await wait(30, "A");
  const b = await wait(10, "B");
  console.log(`${a} → ${b}`);
}

main();
解説

await は「そこで止まる」。Bのほうが速いのに後になるのは、Aを待ち終わってから初めてBが開始されるため。これが直列実行。

JS15-D10 成功する処理と失敗する処理を Promise.allSettled でまとめ、それぞれの状態を「fulfilled, rejected」の形式で出力せよ。 コーディング★★☆無料
期待される結果

出力 == fulfilled, rejected

ヒント

allSettled は失敗しても止まらず、各要素の status を教えてくれる。map(r => r.status).join(", ") で取り出す。

模範解答
async function main() {
  const results = await Promise.allSettled([
    Promise.resolve("成功"),
    Promise.reject(new Error("失敗")),
  ]);

  console.log(results.map((r) => r.status).join(", "));
}

main();
解説

Promise.all は1つでも失敗すると全体が失敗するが、allSettled は全部の結果を最後まで集める。「一部が失敗しても残りは使いたい」場面で使う。

4実践シナリオ(5問)

JS15-S01 【擬似API】ユーザーIDを受け取り、30ミリ秒後に { id, name } を返す関数 fetchUser を作れ(id 1 は "田中"、それ以外は "不明")。id=1 で呼び出し「ID:1 田中」の形式で出力すること。 コーディング★★☆無料
期待される結果

出力 == ID:1 田中

ヒント

new Promise の中で setTimeout し、resolve にオブジェクトを渡す。受け取り側は分割代入できる。

模範解答
const fetchUser = (id) =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: id === 1 ? "田中" : "不明" });
    }, 30);
  });

async function main() {
  const { id, name } = await fetchUser(1);
  console.log(`ID:${id} ${name}`);
}

main();
解説

実際のAPI通信もこれと同じ形。次章で学ぶ fetch は、まさにこういう Promise を返す関数になっている。

JS15-S02 【await 忘れの実験】非同期関数の結果を、await ありawait なしの両方で受け取り、次の2行を出力せよ。
「await あり:田中」
「await なし:[object Promise]」
コーディング★★☆無料
期待される結果

出力2行が完全一致

ヒント

await なしの場合はテンプレートリテラルに入れると [object Promise] と表示される。

模範解答
const getName = async () => "田中";

async function main() {
  const withAwait = await getName();
  const withoutAwait = getName();

  console.log(`await あり:${withAwait}`);
  console.log(`await なし:${withoutAwait}`);
}

main();
解説

この [object Promise] を見たら await の付け忘れと即座に判断できるようにしておきたい。画面に出てしまう典型的なバグ。

JS15-S03 【直列と並列の違い】30ミリ秒かかる処理を3つ実行する。まず直列で実行して「直列:A,B,C」と出力し、次に並列で実行して「並列:A,B,C」と出力せよ。さらに「並列のほうが速い:true」と出力すること(かかった時間を比較する)。 コーディング★★☆無料
期待される結果

出力3行が完全一致

ヒント

Date.now() で開始時刻を記録し、終了時刻との差を測る。直列は await を3回、並列は Promise.all。

模範解答
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

async function main() {
  const t1 = Date.now();
  const a = await wait(30, "A");
  const b = await wait(30, "B");
  const c = await wait(30, "C");
  const serial = Date.now() - t1;
  console.log(`直列:${a},${b},${c}`);

  const t2 = Date.now();
  const results = await Promise.all([wait(30, "A"), wait(30, "B"), wait(30, "C")]);
  const parallel = Date.now() - t2;
  console.log(`並列:${results.join(",")}`);

  console.log(`並列のほうが速い:${parallel < serial}`);
}

main();
解説

結果は同じでも、かかる時間が3倍違う。直列は約90ms、並列は約30ms。互いに依存しない処理を直列で書くのは、単なる時間の無駄になる。

JS15-S04 【失敗しても続ける】3件のユーザー取得を行う。id=2 だけ失敗する fetchUser を作り、1件失敗しても残りを処理して次の4行を出力せよ。
「[1] 田中」「[2] 取得失敗」「[3] 高橋」「成功2件 / 失敗1件」
コーディング★★★無料
期待される結果

出力4行が完全一致

ヒント

for...of の中で try/catch する。ループの外に成功・失敗のカウンタを用意する。

模範解答
const fetchUser = (id) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 2) {
        reject(new Error("取得失敗"));
        return;
      }
      resolve(id === 1 ? "田中" : "高橋");
    }, 10);
  });

async function main() {
  let ok = 0;
  let ng = 0;

  for (const id of [1, 2, 3]) {
    try {
      const name = await fetchUser(id);
      console.log(`[${id}] ${name}`);
      ok++;
    } catch (e) {
      console.log(`[${id}] ${e.message}`);
      ng++;
    }
  }

  console.log(`成功${ok}件 / 失敗${ng}件`);
}

main();
解説

try/catch をループの中に置くのが要点(JS10と同じ)。外に置くと最初の失敗で全体が止まる。reject を呼ぶと catch に飛ぶ。

JS15-S05 【再試行する】2回失敗してから3回目に成功する処理を、最大3回まで再試行する関数 retry を作れ。試行のたびに「試行1回目...」と出力し、成功したら「成功(3回目)」と出力すること。 コーディング★★★無料
期待される結果

出力4行。試行1〜3回目と成功(3回目)

ヒント

let attempt = 0 を外に置き、for ループの中で try/catch する。成功したら return、失敗なら次の周回へ。

模範解答
let count = 0;

const unstable = () =>
  new Promise((resolve, reject) => {
    count++;
    setTimeout(() => {
      if (count < 3) {
        reject(new Error("失敗"));
        return;
      }
      resolve("データ");
    }, 10);
  });

async function retry(maxAttempts) {
  for (let i = 1; i <= maxAttempts; i++) {
    console.log(`試行${i}回目...`);

    try {
      await unstable();
      console.log(`成功(${i}回目)`);
      return;
    } catch (e) {
      if (i === maxAttempts) {
        console.log("すべて失敗しました");
      }
    }
  }
}

retry(3);
解説

通信は一時的に失敗することがあるので、実務では再試行が標準的に組み込まれる。本来は失敗のたびに待ち時間を延ばす(指数バックオフ)が、まずはこの形を押さえたい。

5仕上げ課題

JS15-FINAL 【商品情報の取得システム】擬似APIから商品データを取得し、レポートを出力せよ。

用意する擬似API(すべて Promise を返す):
fetchProduct(id) … 20ms後に { id, name, price } を返す。id=3 のときは reject(メッセージ「商品が見つかりません」)
 データ:1→ノートPC 128000/2→マウス 3200/4→モニター 45000
fetchStock(id) … 20ms後に在庫数を返す(1→5, 2→0, 4→3)

処理:
① id 1, 2, 3, 4 を順に処理する
② 各IDについて、商品情報と在庫を並列で取得する(Promise.all)
③ 取得できたら「[1] ノートPC:128,000円(在庫5個)」の形式で出力。在庫0なら末尾に「 ※品切れ」を付ける
④ 失敗したら「[3] エラー:商品が見つかりません」と出力し、次へ進む
⑤ 最後に成功件数・失敗件数・取得できた商品の合計金額を出力

出力仕様(6行):
=== 商品情報の取得 ===
[1] ノートPC:128,000円(在庫5個)
[2] マウス:3,200円(在庫0個) ※品切れ
[3] エラー:商品が見つかりません
[4] モニター:45,000円(在庫3個)
成功3件 / 失敗1件 / 合計176,200円
期待される結果

出力6行が完全一致

ヒント

for...of の中で try/catch し、その中で const [product, stock] = await Promise.all([fetchProduct(id), fetchStock(id)]) とする。品切れの表示は三項演算子で。合計は成功したものだけ足す。

模範解答
const PRODUCTS = {
  1: { name: "ノートPC", price: 128000 },
  2: { name: "マウス", price: 3200 },
  4: { name: "モニター", price: 45000 },
};

const STOCKS = { 1: 5, 2: 0, 4: 3 };

const fetchProduct = (id) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!PRODUCTS[id]) {
        reject(new Error("商品が見つかりません"));
        return;
      }
      resolve({ id, ...PRODUCTS[id] });
    }, 20);
  });

const fetchStock = (id) =>
  new Promise((resolve) => {
    setTimeout(() => resolve(STOCKS[id] ?? 0), 20);
  });

async function main() {
  console.log("=== 商品情報の取得 ===");

  let ok = 0;
  let ng = 0;
  let total = 0;

  for (const id of [1, 2, 3, 4]) {
    try {
      const [product, stock] = await Promise.all([fetchProduct(id), fetchStock(id)]);
      const mark = stock === 0 ? " ※品切れ" : "";

      console.log(`[${id}] ${product.name}:${product.price.toLocaleString()}円(在庫${stock}個)${mark}`);

      total += product.price;
      ok++;
    } catch (e) {
      console.log(`[${id}] エラー:${e.message}`);
      ng++;
    }
  }

  console.log(`成功${ok}件 / 失敗${ng}件 / 合計${total.toLocaleString()}円`);
}

main();
解説

1件ごとに商品情報と在庫を並列取得しつつ、失敗しても次へ進む——実務のAPI連携そのものの構造になっている。

設計上の要点は2つ。第一に、商品と在庫は互いに依存しないので Promise.all で同時に取る。順に await すると1件あたり40msかかるが、並列なら20msで済む。第二に、try/catch をループの中に置いているので、id=3 で失敗しても id=4 の取得は続く。外に置いていたら、そこで全部止まっていた。

この章で最も大事なのは、await を付けたら「そこで処理が止まる」という感覚だ。止めるべきところでは止め、止める必要がないところは Promise.all で同時に走らせる——その判断ができれば、非同期処理は怖くない。

次章では、この擬似APIを本物のAPIに置き換える。fetch を学べば、世界中のサービスからデータを取得できるようになる。