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=「awaitを使える関数」にする合図
awaitを使いたい関数には、まずasyncを付けます。これでその関数は「非同期OKモード」になります。
async function getUser() {
// ここで await が使える
}
よくあるミス:awaitだけ書いて「awaitはasyncの外じゃ使えません」って怒られるやつです。先にasync、覚えてください。
await=「Promiseの結果を待つ」合図(待つ間は他に譲る)
awaitは Promiseが終わるまで待って、結果を受け取るための札です。
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**を付けます。※awaitはasyncの中でだけ使えます。
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が出るんですけど!?」の正体です。待ってないだけでした。

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);
}
}
※地味に大事:fetchは404とかでも(通信自体は成功なので)普通にresが返ることがあります。だからres.okチェックが効きます。
失敗しやすいポイント(URL間違い/JSONじゃない等)
ハマりがちなのはここです👇
困ったら、まずは「画面に失敗メッセージ+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を付けて、catchはtry/catchへreturn fetch(...)→const res = await fetch(...)return res.json()→const data = await res.json().catch((err)=>{...})→try { ... } catch (err) { ... }
置き換えルール(ここだけ覚える)
逆に「上にも伝えたい」なら、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つです。
超ミニのリトライ例(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 p | 1人が来るのを待つ |
| 順番に待つ | for...of + await | 1人ずつ呼ぶ(行列) |
| 同時に待つ | 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}`))
);
判断フロー(これだけ)
よくある事故: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);
}
じゃあ「失敗しても全部の結果が欲しい」なら?
そんなときは次のどっちかで考えるのがラクです。
const settled = await Promise.allSettled(
ids.map((id) => fetch(`/api/user/${id}`))
);
// settledには {status:"fulfilled", value:...} / {status:"rejected", reason:...} が入る
よくある質問
- Qasyncを付けたら戻り値はどうなる?
- A
必ずPromiseになります。
たとえ中でreturn 123しても、外から見ると「Promiseで123を返す」になります。async function f() { return 123; } f().then(console.log); // 123
- Qawaitを書かなくても「動く」のはなぜ?
- A
待ってないだけで、処理自体は走ってます。
結果を受け取る前に次へ進むので、Promise { <pending> }が出たりします。const p = fetch("/api"); // awaitなし console.log(p); // Promise...
- QawaitってPromiseじゃないものにも付けられる?
- A
付けられます(その場合は即座に値になります)。
でも「待つ意味」はほぼないので、基本はPromiseに使うのが安心です。const n = await 10; // 10
- Qawaitすると画面やアプリ全体が止まりますか?
- A
止まりません。止まるのは“その関数の続き”だけです。
他の処理は動けます(だから「待ち合わせ」の例がしっくり来ます)。
- Qfetchで404でもcatchに行かないのはなぜ?
- A
通信自体は成功扱いだからです(resは返る)。
なのでres.okやres.statusを自分でチェックします。const res = await fetch(url); if (!res.ok) throw new Error(res.status);
- Qtry/catchはどこまで囲えばいい?
- A
awaitする処理+その結果を使うまとまりを1セットで囲うのが基本です。
「この機能1個分」で包むと迷いにくいです。try { const res = await fetch(url); const data = await res.json(); } catch (e) {}
- QforEachの中でawaitが効かないのはなぜ?
- A
forEachは“待つ係”じゃなくて、呼ぶだけだからです。
代わりに 順番ならfor...of、まとめてならmap + Promise.allです。await Promise.all(arr.map(async x => work(x)));
- QPromise.allって何がそんなに怖いの?
- A
1つでも失敗したら全体が失敗になる(途中でcatchへ)からです。
「成功/失敗を全部集めたい」ならPromise.allSettledが便利です。const r = await Promise.allSettled(tasks);
- Qawaitをいっぱい書くと遅くなりますか?
- A
順番待ちになるので、必要以上に直列にすると遅くなりがちです。
同時にやってOKなものはPromise.allでまとめると速くなることが多いです。
- Qthenとawaitは混ぜてもいい?
- A
動きますが、混ぜると読みづらくなりがちです。
基本は「awaitで統一」、どうしても必要な場所だけthen、が安全です。const data = await fetch(url).then(r => r.json()); // 動くけど統一推奨
