- まず「Promise の落とし穴」をざっくり整理する
- 落とし穴1:then の中で return を忘れてチェーンが切れる
- 落とし穴2:非同期の結果を「外の変数に代入してすぐ使おうとする」
- 落とし穴3:エラーを catch しない(UnhandledPromiseRejection)
- 落とし穴4:then の第2引数でエラーを拾ってしまって、後ろに伝わらない
- 落とし穴5:forEach と async/await(や Promise)を組み合わせて「逐次処理」のつもりになる
- 落とし穴6:Promise コンストラクタの過剰使用(無意味な二重ラップ)
- 落とし穴7:Promise の「非同期性」を忘れて順序を勘違いする
- まとめ:Promise の落とし穴を避けるための “思考のクセ”
まず「Promise の落とし穴」をざっくり整理する
Promise 自体は仕組みとしてはそんなに難しくないのに、
実際に書き始めると「なんか思った通りに動かない」「エラーが消える」「順番がおかしい」みたいな、
モヤっとする挙動にハマりがちです。
そのほとんどは、
「then や catch が何を返しているかを意識していない」
「Promise の“非同期性”を、同期の感覚で扱ってしまう」
ここから来ます。
ここが重要です。
Promise の落とし穴は、“言語仕様の罠” というより、
「値・エラー・タイミングの流れを、自分の頭でちゃんと追えているか」 のところでほぼ決まります。
その視点で、一つずつ噛み砕いていきます。
落とし穴1:then の中で return を忘れてチェーンが切れる
何が起きるか
典型的なのがこれです。
fetchUser()
.then((user) => {
console.log("user:", user);
fetchPosts(user.id); // ここで return してない
})
.then((posts) => {
console.log("posts:", posts); // posts が undefined だったり、タイミングがズレたりする
})
.catch((err) => {
console.error("エラー:", err);
});
JavaScript「ユーザーを取ってから投稿を取るつもり」が、
なぜか posts が undefined になったり、fetchPosts の結果がこのチェーンに乗らなかったりします。
理由はシンプルで、
「1 つ目の then の戻り値が undefined になっているから」 です。
正しい動きと比較してみる
正しくはこうです。
fetchUser()
.then((user) => {
console.log("user:", user);
return fetchPosts(user.id); // Promise を return する
})
.then((posts) => {
console.log("posts:", posts); // ここに fetchPosts の結果が来る
});
JavaScriptPromise のルールでは、
then の中で return したものが
→ 次の then の入力になる
です。
「return を書き忘れる」=「暗黙に undefined を返す」
→ 次の then は「undefined で成功した Promise」として動き出す
→ 期待していた Promise チェーンは切れてしまう
という流れになります。
ここが重要です。
「then の最後は return で終わる」が基本ルール。
特に次の非同期処理(Promise)を呼ぶときは、必ず return xxxPromise(...) と書く癖をつけると、安全度が一気に上がります。
落とし穴2:非同期の結果を「外の変数に代入してすぐ使おうとする」
よくあるコード
これも初心者が必ず一度はやるやつです。
let data;
fetchSomething().then((result) => {
data = result;
});
console.log(data); // まだ undefined
JavaScript「あとで data を使いたいから外に出しておこう」という発想ですが、
Promise は「非同期」なので、console.log が走る時点では、
まだ then が実行されていない可能性が高いです。
結果として「なぜか常に undefined」という状態になります。
なぜそうなるか(タイミングの問題)
Promise をざっくり時間軸で見ると、
fetchSomething()を呼ぶ(非同期処理スタート)- すぐに
thenの登録だけされる(実行はあと) - 同期コードがそのまま進み、
console.log(data)が走る(この時点ではまだdataは代入されていない) - 非同期処理が終わったタイミングで、
thenの中が走る
という順番になっています。
Promise の世界では、
「外に変数を置いて、後でそこに結果をセット」という書き方をしようとするほど、タイミングの罠にハマる
と思っておいた方がいいです。
どう書き換えるべきか
「結果を使いたい処理」は、
Promise の中に“ぶらさげる” のが基本です。
fetchSomething()
.then((data) => {
console.log("ここで data を使う:", data);
})
.catch((err) => {
console.error("エラー:", err);
});
JavaScriptあるいは、関数として次につなぐ。
function handleData(data) {
console.log("ここで data を使う:", data);
}
fetchSomething().then(handleData);
JavaScriptここが重要です。
Promise の結果を「外に持ち出してあとで同期的に使う」のではなく、「結果を使う処理ごと then / async 関数の中に入れる」。
非同期の世界の中で完結させるのが、タイミングの落とし穴を避ける一番のコツです。
落とし穴3:エラーを catch しない(UnhandledPromiseRejection)
何が問題になるか
Promise の中でエラーが起きても、
どこにも catch がないと、環境によっては
「UnhandledPromiseRejectionWarning」
「未処理の Promise 拒否」
のような警告が出ます。
最近のブラウザや Node では、ときにこれは致命的とみなされることもあります。
例:
new Promise((resolve, reject) => {
reject(new Error("何かがおかしい"));
});
// catch なし
JavaScriptどこか一箇所でいいから必ず catch をつける
Promise チェーンのどこかで失敗する可能性があるなら、
一番最後に catch を 1 個つけておく のがおすすめです。
doStep1()
.then(doStep2)
.then(doStep3)
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScriptdoStepX のどこで reject や throw があっても、
最後の catch が全部まとめて拾ってくれます。
ここが重要です。
「成功の流れ(then)はよく書くのに、エラーの出口(catch)を忘れがち」なのが落とし穴。
「Promise チェーンには必ず 1 箇所以上 catch をつける」を習慣化すると、意味不明な未処理エラーから解放されます。
落とし穴4:then の第2引数でエラーを拾ってしまって、後ろに伝わらない
こういう書き方
Promise は、then に第2引数を渡せます。
promise.then(
(value) => { /* 成功時 */ },
(error) => { /* 失敗時 */ }
);
JavaScriptしかし、これを多用すると、
エラー伝播が分かりづらくなります。
doSomething()
.then(
(value) => {
console.log("成功:", value);
},
(error) => {
console.error("ここでエラー処理:", error);
// ここで throw しない場合、エラーは「処理された」とみなされる
}
)
.then(() => {
console.log("ここは普通に実行されてしまうこともある");
})
.catch((err) => {
console.error("最後の catch:", err);
});
JavaScript何が起きているか
第2引数のエラーハンドラ(onRejected)でエラーを処理すると、
そこで「エラーが消化された」と解釈されます。
そのとき何も throw しなければ、
次の then は「成功」として動き始めます。
結果として、
「エラーが起きたのに、なぜか後ろの then が普通に動いている」
という混乱が起きます。
解決策:エラーは catch に任せる
実務的なおすすめは、
then には成功時だけを書く
エラーは最後の catch(または途中の catch)でまとめて扱う
というパターンに統一することです。
doSomething()
.then((value) => {
console.log("成功:", value);
})
.then(() => {
console.log("さらに続き");
})
.catch((err) => {
console.error("どこかで失敗:", err);
});
JavaScriptここが重要です。
「成功は then」「失敗は catch」という役割分担にしておくと、
エラーがどこで止まるか分からない、という Promise 特有の混乱をかなり減らせます。
落とし穴5:forEach と async/await(や Promise)を組み合わせて「逐次処理」のつもりになる
よくある誤解
例えば、「配列の要素を順番に非同期処理したい」とします。
こう書いてしまうことがあります。
const urls = ["/a", "/b", "/c"];
urls.forEach((url) => {
fetch(url)
.then((res) => res.text())
.then((text) => console.log(url, "完了"));
});
console.log("全部終わり");
JavaScriptあるいは async/await でも:
urls.forEach(async (url) => {
const res = await fetch(url);
const text = await res.text();
console.log(url, "完了");
});
console.log("全部終わり");
JavaScript「forEach で順番にやってるつもり」
でも、実際には全部同時にスタートしていて、console.log("全部終わり") のほうが先に出ます。
なぜか
forEach 自体は同期的に、内側のコールバックを次々呼びます。
その中で fetch などの非同期を呼んでも、forEach は「その完了を待たない」のがポイントです。
async/await になっても、
urls.forEach(async (url) => {
await fetch(url);
});
JavaScriptという書き方では、「外側の forEach は待っていない」ので、
「全部終わってから次へ進む」保証はありません。
正しい逐次処理/並列処理の書き方
逐次処理にしたいなら、for 文や reduce+Promise チェーンを使うべきです。
async function main() {
for (const url of urls) {
const res = await fetch(url);
const text = await res.text();
console.log(url, "完了");
}
console.log("全部順番に終わり");
}
JavaScriptPromise チェーンなら:
urls
.reduce((prev, url) => {
return prev.then(() => {
return fetch(url).then((res) => res.text());
});
}, Promise.resolve())
.then(() => {
console.log("全部順番に終わり");
});
JavaScriptここが重要です。
「forEach の中で async/await(や Promise)を書いても、“forEach 自体” は待たない」。
配列を非同期で扱うときは、「逐次にしたいのか」「並列にしたいのか」を意識して、構造(for / Promise.all など)を選ぶ必要があります。
落とし穴6:Promise コンストラクタの過剰使用(無意味な二重ラップ)
ありがちなアンチパターン
fetch や既に Promise を返す関数に対して、
こんなラップを書いてしまうことがあります。
function getData() {
return new Promise((resolve, reject) => {
fetch("/api/data")
.then((res) => res.json())
.then((data) => resolve(data))
.catch((err) => reject(err));
});
}
JavaScript一見よさそうですが、これは
「Promise を返す fetch を、同じ意味の Promise で包み直しているだけ」
になっていて、全く意味がありません。
どう書くべきか
この場合、resolve / reject で中継せず、
そのままチェーンを返せばよいです。
function getData() {
return fetch("/api/data").then((res) => res.json());
}
JavaScriptあるいは async/await なら:
async function getData() {
const res = await fetch("/api/data");
return res.json();
}
JavaScriptnew Promise を使う必要があるのは、
「既存のAPIがコールバックベース」だったり、
「タイムアウトやリトライなど、独自の制御をしたいとき」です。
ここが重要です。
「元から Promise なもの」を、意味もなく new Promise で包み直すのはアンチパターン。
“Promise を返さないもの” を Promise 化するときにだけ、new Promise を使うのが基本です。
落とし穴7:Promise の「非同期性」を忘れて順序を勘違いする
ログの順序で混乱するパターン
こんなコードを考えます。
console.log("A");
Promise.resolve()
.then(() => {
console.log("B");
});
console.log("C");
JavaScript直感的には「A → B → C」と出そうですが、
実際の出力は「A → C → B」です。
理由は、
Promise の then は、「今の同期処理が全部終わったあと」に実行される
(マイクロタスクとしてイベントループに登録される)
からです。
最初に this を理解していないと、 「なんで B が最後なの?」という謎現象に見えます。
実務での影響
Promise の中の処理は、
「必ず少なくとも 1 tick 後に実行される」 と考えたほうが安全です。
「then の中でフラグを true にして、すぐ後ろの同期コードでそれを頼りに何かする」
みたいな書き方をすると、タイミングがズレてバグになります。
ここが重要です。
「then の中身は、“今の処理の直後”ではなく、“今の処理が一段落してから”実行される」。
Promise の処理はいつも「少し後で動く」と頭に置いて、同期処理と混ぜて考えすぎないこと。
まとめ:Promise の落とし穴を避けるための “思考のクセ”
ここまでの落とし穴は、どれも根っこは同じです。
「今この then は何を return しているか?」
「その値(またはエラー)は、次のどこに渡っているか?」
「この処理はいつ(同期/非同期のどのタイミングで)実行されるか?」
これを意識して追っていないと、「なんとなく書いた Promise」が不思議な挙動をし始めます。
逆に言えば、
ここが重要です。
“then の return” と “catch でのエラーの流れ” と “非同期のタイミング” を丁寧に意識できるようになれば、Promise の落とし穴のほとんどは自然と避けられるようになります。
おすすめの練習は、次のようなことです。
小さな Promise チェーンを書いて、
各 then / catch の先頭で console.log("ここ", 値) を入れてみる
「どの順番でどんな値が来ているか」「どこでエラーが catch に飛ぶか」を、自分の目で追ってみる
これを何パターンかやると、
Promise の中で「値とエラーがどう流れているか」「いつ実行されているか」が感覚としてつかめてきて、
落とし穴にハマる回数が確実に減っていきます。

