「try / catch の範囲」を一言でいうと
try / catch の「範囲」は、
「どこで起きたエラーまで、この try / catch で捕まえられるのか」 という話です。
ここを勘違いすると、
「ちゃんと try / catch 書いたのに、エラーが捕まらない」
「非同期処理のエラーだけ素通りしてコンソールに出る」
という、よくあるモヤモヤにハマります。
特に非同期処理(fetch, setTimeout, Promise)と組み合わさると、
「見た目は囲えているのに、実際は範囲外」ということが起きやすいです。
ここが重要です。
try / catch の範囲を理解するうえでのキーワードは、
「いつ実行されるか」と「どのスタック(呼び出しの流れ)の中にいるか」 です。
この二つを意識すると、「どこまで届くか」がクリアになります。
同期処理における try / catch の範囲
同期コードでは「呼び出しの流れ」がそのまま範囲になる
まずは素直な同期コードから見てみます。
function c() {
throw new Error("Cでエラー");
}
function b() {
c();
}
function a() {
b();
}
try {
a();
console.log("ここには到達しない");
} catch (err) {
console.log("捕まえた:", err.message);
}
JavaScriptこの場合、エラーは c() の中で起きていますが、b() → a() → try { a(); } という呼び出しの流れ(スタック)の中にいます。
JavaScript は、エラーが投げられたとき、
「今の呼び出しの流れを上にたどっていき、
一番近い try / catch を探す」という動きをします。
だから、try { a(); } の catch でちゃんと捕まえられます。
ここでの「範囲」は、
「try ブロックの中で始まった同期的な処理の呼び出しチェーン全体」 だと思ってください。
つまり、
try の中で呼んだ関数
その関数の中で呼んだ関数
さらにその中で呼んだ関数
この「連なり」の中で起きた同期的な throw は、
全部その try / catch の範囲に入ります。
同期処理の範囲を外れるパターン
逆に、try の外で呼んだ関数の中で throw しても、
その try / catch では捕まえられません。
function doSomething() {
throw new Error("外でエラー");
}
try {
console.log("何か別の処理");
} catch (err) {
console.log("ここでは捕まらない");
}
doSomething(); // ここでエラー
JavaScriptこの場合、doSomething() を呼んでいる場所は try の外なので、
そのエラーはこの try / catch の範囲外です。
ここが重要です。
同期処理では、
「try ブロックの中から始まった呼び出しの流れの中で起きたエラーだけが、その try / catch の範囲」 です。
外から始まったものは、いくら「見た目が近くても」範囲外になります。
非同期処理になると「時間」が範囲を壊し始める
setTimeout の中は「あとで」実行される
次に、非同期の代表として setTimeout を見てみます。
try {
setTimeout(() => {
console.log("コールバック開始");
throw new Error("タイマーの中でエラー");
}, 1000);
} catch (err) {
console.log("捕まえた:", err.message);
}
JavaScript一見、「ちゃんと try / catch で囲んでいる」ように見えますが、
この catch は呼ばれません。
理由はシンプルで、
setTimeout のコールバックが実行されるのは「1秒後」だからです。
タイムラインで整理するとこうなります。
- try ブロックが実行される
- setTimeout が「1秒後にこの関数を実行してね」と予約する
- try ブロックは何事もなく終わり、catch もスルーされる
- 1秒後、コールバック関数が別タイミングで実行される
- その中で throw が起きるが、そのときにはもう try / catch は存在しない
つまり、
「try / catch の範囲」は、“その瞬間に実行されている同期的な処理” にしか効かない のです。
setTimeout のコールバックは、「別のタイミングで、別のスタックとして」実行されるので、
外側の try / catch の範囲から外れてしまいます。
fetch(Promise)も同じく「あとで」完了する
fetch も非同期です。
Promise ベースで動いているので、
エラーが起きるのは「Promise が完了するとき」です。
try {
fetch("https://example.com/api/data")
.then((response) => {
throw new Error("then の中でエラー");
});
} catch (err) {
console.log("捕まえた:", err.message);
}
JavaScriptこれも catch には入りません。
then のコールバックが呼ばれるのは、
fetch が終わって Promise が resolve された「あと」です。
そのタイミングでは、外側の try / catch はもう通り過ぎています。
ここが重要です。
非同期処理では、
「時間がずれた瞬間に、その処理は“別世界”のスタックで動き始める」 と考えてください。
その別世界で起きたエラーは、外側の同期的な try / catch の範囲には入りません。
Promise チェーンにおける「範囲」は catch が決める
then / catch の世界では「最後の catch まで」が範囲
Promise チェーンでは、
try / catch ではなく .catch(...) が「範囲」を決めます。
fetch("https://example.com/api/data")
.then((response) => {
if (!response.ok) {
throw new Error("HTTP エラー: " + response.status);
}
return response.json();
})
.then((data) => {
console.log("成功:", data);
})
.catch((err) => {
console.log("Promise チェーンで捕まえた:", err.message);
});
JavaScriptここでは、
fetch の中で起きたネットワークエラー
最初の then の中で throw したエラー
二つ目の then の中で throw したエラー
これらはすべて、最後の .catch(...) に流れてきます。
Promise の世界では、
「そのチェーンの中で起きたエラーは、下流の catch まで流れていく」
というルールがあります。
逆に言うと、
catch を付け忘れた Promise チェーンの中でエラーが起きると、
「未処理の Promise 拒否」としてコンソールに警告が出るだけで、
どこにもちゃんと処理されません。
ここが重要です。
Promise を使うときの「範囲」は、
「このチェーンの最後の catch まで」 です。
同期の try / catch とは別のレイヤーで、「Promise 用の範囲」があるイメージを持ってください。
async / await の try / catch の範囲
async 関数の中では「await しているところまで」が範囲に入る
async / await を使うと、
非同期処理を同期っぽく書ける、とよく言われます。
例えば、次のコードを見てください。
async function loadData() {
try {
const response = await fetch("https://example.com/api/data");
if (!response.ok) {
throw new Error("HTTP エラー: " + response.status);
}
const data = await response.json();
console.log("成功:", data);
} catch (err) {
console.log("loadData 内で捕まえた:", err.message);
}
}
JavaScriptここでは、
fetch がネットワークエラーで reject した場合
response.json() が失敗した場合
自分で throw した HTTP エラー
これらはすべて、同じ try / catch で捕まえられます。
なぜかというと、await は「Promise の完了をこの行で待つ」ので、
reject されたときに「ここで throw されたかのように」扱われるからです。
つまり、
「await を書いた場所は、非同期のエラーを同期例外に変換するポイント」 です。
その結果、
async 関数の中の try / catch は、
「その関数の中で await している非同期処理のエラー」まで範囲に含めることができます。
async 関数を呼ぶ側の範囲に注意
ただし、呼び出し側で await しないと、
その外側の try / catch では捕まえられません。
async function loadData() {
const response = await fetch("https://example.com/api/data");
const data = await response.json();
return data;
}
try {
loadData(); // await していない
} catch (err) {
console.log("ここでは捕まらない:", err.message);
}
JavaScriptこの場合、loadData() は Promise を返しているだけで、
その場ではまだ何も起きていません。
エラーが起きるのは「あとで」(非同期)なので、
同期的な try / catch の範囲には入りません。
正しくは、呼び出し側でも await します。
try {
const data = await loadData();
console.log("データ:", data);
} catch (err) {
console.log("呼び出し側で捕まえた:", err.message);
}
JavaScriptここが重要です。
async / await の世界では、
「async 関数の中の try / catch」
→ その関数の中の await までを範囲に含む。
「async 関数を呼ぶ側の try / catch」
→ その async 関数を await したときに起きるエラーを範囲に含む。
という二重構造になっています。
“await していない Promise は、同期の try / catch の範囲外” という感覚を、何度も意識してほしいところです。
実務で意識したい「どこまでを一つの try / catch に入れるか」
何でもかんでも一つの try に詰め込まない
例えば、こんなコードはよくありません。
async function handleRequest() {
try {
const response = await fetch("/api/data");
const data = await response.json();
const processed = processData(data);
updateUI(processed);
saveToLocalStorage(processed);
} catch (err) {
console.error("何かが失敗した:", err);
}
}
JavaScriptこれだと、
fetch の失敗
JSON パースの失敗
データ加工のバグ
UI 更新のバグ
ローカルストレージ保存の失敗
全部が一つの catch に流れ込みます。
ログには「何かが失敗した」としか出ず、
どこで何が起きたのか分かりにくくなります。
「責務ごと」に try / catch を分ける
少し丁寧に分けると、こうなります。
async function handleRequest() {
let data;
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error("HTTP エラー: " + response.status);
}
data = await response.json();
} catch (err) {
console.error("通信または JSON の取得に失敗:", err.message);
return;
}
let processed;
try {
processed = processData(data);
} catch (err) {
console.error("データ加工に失敗:", err.message);
return;
}
try {
updateUI(processed);
saveToLocalStorage(processed);
} catch (err) {
console.error("UI 更新または保存に失敗:", err.message);
}
}
JavaScriptこうすると、
通信・JSON 取得の失敗
データ加工の失敗
UI 更新・保存の失敗
が、それぞれ別のメッセージでログに出ます。
ここが重要です。
try / catch の「範囲」は、
「どの処理をひとまとめにして、“同じ種類の失敗” とみなすか」
という設計の単位でもあります。
非同期処理だからといって一つにまとめるのではなく、
「ここからここまでは通信」「ここからここまではデータ加工」といった意味の塊ごとに try / catch を分けると、
エラーの原因が追いやすくなります。
初心者として「try / catch の範囲」で本当に押さえてほしいこと
同期処理では、
「try ブロックの中から始まった呼び出しの流れの中で起きたエラー」が範囲に入る。
非同期処理(setTimeout, fetch, Promise の then など)では、
「あとで実行されるコールバックの中のエラー」は、
外側の同期的な try / catch の範囲には入らない。
Promise チェーンでは、
「そのチェーンの中で起きたエラー」は、
下流の .catch(...) が範囲を決める。
async / await では、await が「Promise のエラーを同期例外に変換するポイント」になり、
async 関数の中の try / catch は、その関数内の await までを範囲に含める。
呼び出し側でエラーを捕まえたいなら、その async 関数を必ず await する。
そして、実務では、
「どこからどこまでを一つの try / catch に入れるか」を、
処理の意味(通信・変換・UI など)ごとに意識して分けると、
エラーの原因が見えやすくなる。
ここが重要です。
コードを書くとき、
「この try / catch は、どの処理の“責任範囲”を守っているのか?」
「このエラーは、いつ・どのスタックで起きるのか?」
と自分に問いかけてみてください。
その問いを繰り返すうちに、
try / catch は「とりあえず囲むおまじない」ではなく、
“時間と責務を区切るための道具” として、ちゃんと使いこなせるようになっていきます。
