スポンサーリンク

Promiseのごちゃごちゃ卒業!async/awaitがスッとわかる入門

スポンサーリンク
この記事は約20分で読めます。
スポンサーリンク

Promiseのthen()が増えてきて、気づいたらコードが階段みたいに右へ右へ…ってなってません?
awaitってどこに置くの?」「asyncって誰に付けるの?」「え、Promise { <pending> }って何!?」みたいなやつ、めちゃ起きがちです。

Aさん「then/catchが長すぎて読めないです…」
Bさん「画面が更新されないんですけど!?(待ってないだけでした)」
Cさん「forEachの中でawaitしたのに、全然待ってくれません…」

大丈夫です。この記事では、Promiseのごちゃごちゃを卒業して、async/awaitを“ちゃんと使える形”でスッと理解できるようにします。
特に、つまずきやすい ①awaitすると何が起きるか ②fetchの定番の型 ③try/catchの考え方 ④ループ(forEach問題)とPromise.allの使い分け まで、最小サンプルで順番に片づけます。

難しい言葉で暗記するんじゃなくて、「待ち合わせ」みたいなたとえで動きのイメージを作ってからコードに戻るので、読み終わるころには「なるほど、ここで待って、ここで拾うのね」が自然にわかるはずです。さっそくいきましょう。

スポンサーリンク

まず1行で結論:asyncとawaitは何?

結論:asyncは「この関数の中でawait使ってOKですよ」の名札、awaitは「このPromise、結果出るまで待ちますよ」の札です。

迷う場所はだいたいここだけです👇

  • async関数に付ける(関数宣言/関数式/アロー関数)
  • awaitPromiseの前に付ける
  • awaitasyncの中でだけ使える(基本ルール)

async=「awaitを使える関数」にする合図

awaitを使いたい関数には、まずasyncを付けます。これでその関数は「非同期OKモード」になります。

async function getUser() {
  // ここで await が使える
}

よくあるミスawaitだけ書いて「awaitはasyncの外じゃ使えません」って怒られるやつです。先にasync、覚えてください。

await=「Promiseの結果を待つ」合図(待つ間は他に譲る)

awaitPromiseが終わるまで待って、結果を受け取るための札です。

async function main() {
  const res = await fetch("/api/user"); // Promiseの前にawait
  console.log(res);
}

ここ大事なんですが、awaitで待ってる間、プログラム全体が固まるわけじゃないです。
「この関数はいったん休憩、他の処理は先にどうぞ〜」って感じで、あとで続きが再開されます。

ステップアップ最小サンプル:setTimeout → Promise → await

ここは「いきなりfetchはしんどい…」って人のための、超ミニ練習コースです。
毎回、予想→実行→結果でいきますね。

setTimeoutだけだと「結果」を受け取れない

予想:1秒後に42が返ってくる?…と思いきや、返ってきません。

function getNumber() {
  setTimeout(() => {
    return 42; // ← これ、外には返せてない
  }, 1000);
}

const n = getNumber();
console.log(n);

結果(console)

undefined

setTimeoutの中は「あとで実行」なので、外側のgetNumber()は先に終わっちゃうんです。

Promiseにすると「あとで結果を渡す約束」になる

今度は「1秒後に42を渡しますね」という**約束(Promise)**にします。

function getNumberPromise() {
  return new Promise((resolve) => {
    setTimeout(() => resolve(42), 1000);
  });
}

const p = getNumberPromise();
console.log(p);

結果(console)

Promise { <pending> } // まだ結果待ち

「じゃあ結果はどう受け取るの?」→ Promiseだとこうです。

getNumberPromise().then((n) => console.log(n));

結果(console)

42

awaitにすると「上から読める」になる(thenの置き換え)

thenの代わりに、**Promiseの前にawait**を付けます。※awaitasyncの中でだけ使えます。

async function main() {
  const n = await getNumberPromise();
  console.log(n);
}

main();

結果(console)

42

ついでに、わざと事故も体験しておきます👇(これ超あるあるです)

async function main() {
  const n = getNumberPromise(); // ← await忘れ!
  console.log(n);
}
main();

結果(console)

Promise { <pending> }

はい、これが「Promiseが出るんですけど!?」の正体です。待ってないだけでした。

Visual Studio Code - The open source AI code editor
Visual Studio Code redefines AI-powered coding with GitHub Copilot for building and debugging modern web and cloud appli...
Chrome DevTools  |  Chrome for Developers
Chrome DevTools を使用して、ウェブ アプリケーションのデバッグと最適化を行います。

fetchの定番:まずこの型を覚える(コピペOK)

「async/awaitはわかったけど、結局fetchどう書くの?」ってときは、まずこれを固定の型として覚えちゃってOKです。変えるのはだいたい URL成功した後の処理 だけです。

resとjsonの2段階await(なぜ2回待つ?)

ポイントはawaitが2回あるところです。
1回目は「通信が終わってResponseが届くまで待つ」、2回目は「中身(JSON)を読み取るまで待つ」です。

async function loadUser() {
  const messageEl = document.querySelector("#message");

  try {
    const res = await fetch("/api/user"); // ① 通信を待つ

    if (!res.ok) {
      throw new Error(`HTTPエラー: ${res.status}`);
    }

    const data = await res.json(); // ② JSON化を待つ(ここも非同期)
    messageEl.textContent = `こんにちは、${data.name}さん`;
  } catch (err) {
    messageEl.textContent = "読み込み失敗…もう一回試してください";
    console.error(err);
  }
}

※地味に大事:fetch404とかでも(通信自体は成功なので)普通にresが返ることがあります。だからres.okチェックが効きます。

失敗しやすいポイント(URL間違い/JSONじゃない等)

ハマりがちなのはここです👇

  • URL間違い/api/userのつづりミス、先頭の/抜け
  • サーバー側がエラーres.okfalseになる(status見て判断)
  • JSONじゃないres.json()で失敗(HTMLが返ってきてる時が多いです)
  • ネットワーク問題:オフラインや一時的な通信不良

困ったら、まずは「画面に失敗メッセージ+consoleに詳細」を出せばOKです(上のテンプレ通り)。これだけで原因特定がめちゃ楽になります。

then/catch地獄を「同じ処理」で書き換える

「これ、動くけど…読めない!」ってなる原因はだいたいこれです👇
.then()が増えるほど、処理の“本体”がどんどん奥に押し込まれていくんですよね。

ここでは、まったく同じ処理を「then版」と「await版」で並べて、差分を見える化します。

then版(読みにくい例)

function loadUser() {
  return fetch("/api/user")
    .then((res) => {
      // ✅ ここでres.okチェック
      if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
      return res.json(); // ✅ 次のthenへ渡す
    })
    .then((data) => {
      // ✅ 画面更新など“本体”が奥に行きがち
      document.querySelector("#message").textContent = `こんにちは、${data.name}さん`;
      return data; // ✅ 呼び出し元に返す
    })
    .catch((err) => {
      // ✅ エラー処理
      document.querySelector("#message").textContent = "読み込み失敗…";
      console.error(err);
      throw err; // ✅ さらに上へ伝えるならthrow
    });
}

「何してるか」を追うために、目線が右へ右へ行きがちです。これが“then地獄”の正体です。

await版(読みやすい例)

async function loadUser() {
  try {
    const res = await fetch("/api/user"); // ★ thenの1段目

    if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);

    const data = await res.json(); // ★ thenの2段目
    document.querySelector("#message").textContent = `こんにちは、${data.name}さん`;

    return data; // ★ thenのreturnは、普通にreturn
  } catch (err) {
    document.querySelector("#message").textContent = "読み込み失敗…";
    console.error(err);
    throw err; // ★ catchでthrowしてたなら、そのままthrow
  }
}

見た目が「上から読める」だけで、やってることは同じです。
ただ、読む側としては “今どこをやってる?” が一瞬でわかるので、保守が楽になります。

書き換え手順3ステップ(機械的に直す)

「センス」じゃなくて、手順で変換しちゃいましょう。

  • 関数にasyncを付ける
    • function loadUser()async function loadUser()
  • thenの中身を上から順に“外へ並べる”
    • then((res) => { ... }){ ... } を、関数の中に移動して順番に書く
  • Promiseの前にawaitを付けて、catchtry/catch
    • return fetch(...)const res = await fetch(...)
    • return res.json()const data = await res.json()
    • .catch((err)=>{...})try { ... } catch (err) { ... }

置き換えルール(ここだけ覚える)

  • thenの中のreturnは、await版では 普通にreturn(または変数に代入して次へ)
  • thenの中のthrowは、await版でも そのままthrow
  • catchで握りつぶしてたなら、await版でも握りつぶしてOK

逆に「上にも伝えたい」なら、throw errで同じ挙動にできます

エラー処理の正解:try/catchは「1セット」で考える

async/awaitで一番ラクになるのが、エラー処理です。
then().catch()で散らばってたのを、「ここからここまでが1つの処理」としてまとめられます。コツはシンプルで、try/catchは1セットで持つ、です。

どこまでをtryに入れる?(待つ処理を囲う)

基本ルールはこれです👇
「awaitする行」と、その結果を使う処理をまとめてtryで囲うのが正解です。

async function loadUser() {
  try {
    const res = await fetch("/api/user");
    if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);

    const data = await res.json();
    document.querySelector("#message").textContent = `こんにちは、${data.name}さん`;
    return data;
  } catch (err) {
    document.querySelector("#message").textContent = "読み込み失敗…";
    console.error(err);
    return null; // ここは設計次第(throwして上に任せてもOK)
  }
}

「どこでcatchする?」って迷ったら、まずはその機能1個分(読み込み1回分)をtry/catchで包む、で大体うまくいきます。

失敗時に何を出す?(画面表示/ログ/再実行)

失敗したときにやりたいことは、だいたいこの3つです。

  • 画面:ユーザーに「失敗した」ってわかる文を出す(専門用語は出さない)
  • ログ:自分用にconsole.error(err)で原因を残す
  • 再実行:必要なら「もう一回」ボタンやリトライを用意する

超ミニのリトライ例(2回まで)も置いておきます👇

async function loadWithRetry() {
  for (let i = 0; i < 2; i++) {
    try {
      const res = await fetch("/api/user");
      if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
      return await res.json();
    } catch (err) {
      console.error("失敗:", err);
      if (i === 1) return null;
    }
  }
}

finallyはいつ使う?(必ず後片付け)

finally成功でも失敗でも必ずやりたい後片付けに使います。
たとえば「読み込み中…」の表示を消す、とかですね。

async function loadUser() {
  const btn = document.querySelector("#loadBtn");
  btn.disabled = true; // 誤連打防止

  try {
    const res = await fetch("/api/user");
    if (!res.ok) throw new Error("読み込み失敗");
    const data = await res.json();
    return data;
  } catch (err) {
    console.error(err);
    return null;
  } finally {
    btn.disabled = false; // ← 成功でも失敗でも戻す
  }
}

ループでハマる所まとめ(forEachでawaitが効かない理由)

結論から言うと、forEachは待ってくれません。なので「順番にやりたいのにバラバラ」「全部終わったと思ったのに終わってない」みたいな事故が起きます。ここは型で覚えるのがいちばん早いです👇

まず練習用に、ちょい待つ関数を用意します。

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

順番にやる:for…of + await

「1個ずつ、順番に」ならこれです。いちばん安全でわかりやすいです。

async function runInOrder() {
  const items = ["A", "B", "C"];

  for (const item of items) {
    await sleep(300);
    console.log(item);
  }

  console.log("全部おわり");
}

runInOrder();

出力はちゃんと A → B → C → 全部おわり になります。
「順番が大事」「サーバーに負担かけたくない」なら、まずこれでOKです。

まとめてやる:Promise.all(怖くない使い方)

「同時にやってOK」「早く終わらせたい」ならこれです。ポイントは、先に“約束(Promise)”を配列にしてからまとめて待つことです。

async function runTogether() {
  const items = ["A", "B", "C"];

  const promises = items.map(async (item) => {
    await sleep(300);
    console.log(item);
    return item;
  });

  const results = await Promise.all(promises);
  console.log("結果:", results);
}

runTogether();

※同時に動くので、A/B/Cの表示順は前後することがあります(そこが「同時」の特徴です)。
でも最後のPromise.allで「全部そろうまで待つ」ができます。

forEach内awaitが効かない理由と代案(map + Promise.all)

ダメな例を先に見ます👇

async function bad() {
  const items = ["A", "B", "C"];

  items.forEach(async (item) => {
    await sleep(300);
    console.log(item);
  });

  console.log("forEach終わり"); // ← これが先に出ちゃう
}

bad();

なぜかというと、forEachは「中の処理が終わるまで待つ」係じゃなくて、「はい次、はい次」って呼ぶだけだからです。中でawaitしてても、外側は知らんぷりで先に進みます。

代案はこれ。map + Promise.allで「全部待つ」を作れます。

async function good() {
  const items = ["A", "B", "C"];

  await Promise.all(
    items.map(async (item) => {
      await sleep(300);
      console.log(item);
    })
  );

  console.log("全部おわり");
}

good();

「並列」と「順番に実行」使い分け早見表

迷ったらここに戻ってきてください。選び方は3パターンだけです。

1個だけ待つ/順番に待つ/同時に待つ

やりたいこと書き方ざっくりイメージ
1個だけ待つawait p1人が来るのを待つ
順番に待つfor...of + await1人ずつ呼ぶ(行列)
同時に待つPromise.all([...])みんなに一斉連絡して集合

コードも最小で置いときます👇

// 1個だけ待つ
const data = await fetch("/api/user");
// 順番に待つ
for (const id of ids) {
  const res = await fetch(`/api/user/${id}`);
  console.log(id, res.status);
}
// 同時に待つ
const results = await Promise.all(
  ids.map((id) => fetch(`/api/user/${id}`))
);

判断フロー(これだけ)

  • 順番が大事? → Yes: for...of + await
  • No → 同時にやってOK? → Yes: Promise.all
  • No → そもそも設計を見直す(同時も順番もダメなケース多いです)

よくある事故:Promise.allで1つ落ちたら?(対処の考え方)

Promise.allは基本、1つでも失敗(reject)したら全体が失敗になります。いわゆる「失敗したら即終了(fail fast)」です。

try {
  const results = await Promise.all(tasks);
  console.log(results);
} catch (err) {
  console.error("どれか1つ失敗しました:", err);
}

じゃあ「失敗しても全部の結果が欲しい」なら?
そんなときは次のどっちかで考えるのがラクです。

  • 全部の成功/失敗を集めたいPromise.allSettled
  • 失敗しても続けたい(ただし失敗も記録) → 各Promiseの中でcatchして“失敗も値として返す”
const settled = await Promise.allSettled(
  ids.map((id) => fetch(`/api/user/${id}`))
);
// settledには {status:"fulfilled", value:...} / {status:"rejected", reason:...} が入る

よくある質問

Q
asyncを付けたら戻り値はどうなる?
A

必ずPromiseになります。
たとえ中でreturn 123しても、外から見ると「Promiseで123を返す」になります。

async function f() { return 123; }
f().then(console.log); // 123
Q
awaitを書かなくても「動く」のはなぜ?
A

待ってないだけで、処理自体は走ってます。
結果を受け取る前に次へ進むので、Promise { <pending> }が出たりします。

const p = fetch("/api"); // awaitなし
console.log(p); // Promise...
Q
awaitってPromiseじゃないものにも付けられる?
A

付けられます(その場合は即座に値になります)。
でも「待つ意味」はほぼないので、基本はPromiseに使うのが安心です。

const n = await 10; // 10
Q
awaitすると画面やアプリ全体が止まりますか?
A

止まりません。止まるのは“その関数の続き”だけです。
他の処理は動けます(だから「待ち合わせ」の例がしっくり来ます)。

Q
fetchで404でもcatchに行かないのはなぜ?
A

通信自体は成功扱いだからです(resは返る)。
なのでres.okres.statusを自分でチェックします。

const res = await fetch(url);
if (!res.ok) throw new Error(res.status);
Q
try/catchはどこまで囲えばいい?
A

awaitする処理+その結果を使うまとまりを1セットで囲うのが基本です。
「この機能1個分」で包むと迷いにくいです。

try {
  const res = await fetch(url);
  const data = await res.json();
} catch (e) {}
Q
forEachの中でawaitが効かないのはなぜ?
A

forEachは“待つ係”じゃなくて、呼ぶだけだからです。
代わりに 順番ならfor...ofまとめてならmap + Promise.all です。

await Promise.all(arr.map(async x => work(x)));
Q
Promise.allって何がそんなに怖いの?
A

1つでも失敗したら全体が失敗になる(途中でcatchへ)からです。
「成功/失敗を全部集めたい」ならPromise.allSettledが便利です。

const r = await Promise.allSettled(tasks);
Q
awaitをいっぱい書くと遅くなりますか?
A

順番待ちになるので、必要以上に直列にすると遅くなりがちです。
同時にやってOKなものはPromise.allでまとめると速くなることが多いです。

Q
thenとawaitは混ぜてもいい?
A

動きますが、混ぜると読みづらくなりがちです。
基本は「awaitで統一」、どうしても必要な場所だけthen、が安全です。

const data = await fetch(url).then(r => r.json()); // 動くけど統一推奨

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