「エラー再送出」を一言でいうと
エラー再送出(rethrow)は、
「一度 catch で受け取ったエラーを、“ここでは処理しきらないから、上のレイヤーにもう一回投げ直す” 行為 です。
非同期処理では特に、
・ここではログだけ取りたい
・ここでは共通処理だけしたい
・最終的な判断(画面遷移・ユーザー向け表示)はもっと上の層でやりたい
という場面がよくあります。
そのときに使うのが「再送出」です。
一度 catch して「何かする」、でも「握りつぶさずに上に渡す」——
このバランスを取るためのテクニックだと思ってください。
まずは同期処理で「再送出」のイメージをつかむ
catch したけど、ここでは決めきれない
シンプルな同期コードから見てみます。
function lowLevel() {
throw new Error("一番下でエラー");
}
function middle() {
try {
lowLevel();
} catch (err) {
console.error("[middle] ログだけ取る:", err.message);
throw err; // ← ここが「再送出」
}
}
function top() {
try {
middle();
} catch (err) {
console.log("[top] ユーザー向け処理:", err.message);
}
}
top();
JavaScript流れとしてはこうです。
lowLevel でエラー発生
→ middle の try に飛ぶ
→ middle の catch で一度受け取る
→ ログを出す
→ throw err で上に投げ直す
→ top の catch で最終的な処理をする
ここで重要なのは、
middle は「ログを取る」という責務だけを果たし、
「どうユーザーに伝えるか」は top に任せている ことです。
これが「再送出」の基本パターンです。
「ここで全部決めない。自分の責務だけ果たして、あとは上に任せる」。
非同期処理(async / await)でのエラー再送出
共通関数の中でログ+再送出
非同期処理だと、よくあるのが「共通 API 関数」の中での再送出です。
async function callApi(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const err = new Error("HTTP error " + response.status);
console.error("[callApi] HTTP エラー", { url, status: response.status });
throw err; // ここで throw しているが、これも「上に送る」意味
}
return await response.json();
} catch (err) {
console.error("[callApi] ネットワーク or その他のエラー", err);
throw err; // ← ここが「再送出」
}
}
JavaScriptこの callApi は、こういう役割を持っています。
・fetch の実行
・HTTP ステータスのチェック
・ログ出力
・エラーを「握りつぶさずに」呼び出し元に伝える
呼び出し側はこう書けます。
async function loadUsers() {
try {
const data = await callApi("/api/users");
renderUsers(data);
} catch (err) {
console.log("[loadUsers] ここでユーザー向けエラー表示を決める");
showErrorMessage("ユーザーの取得に失敗しました。時間をおいて再度お試しください。");
}
}
JavaScriptここが重要です。
callApi の catch で「ログだけ取り、再送出する」ことで、
「技術的な共通処理」と「画面ごとの振る舞い」をきれいに分離できる のです。
「再送出しない」と何がまずいのか
その場で握りつぶすと「上のレイヤー」が何も知らない
よくある悪いパターンがこれです。
async function callApi(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return await response.json();
} catch (err) {
console.error("[callApi] エラー:", err);
// 何も throw しない
}
}
async function loadUsers() {
try {
const data = await callApi("/api/users");
renderUsers(data); // data が undefined かもしれない
} catch (err) {
// ここには来ない
showErrorMessage("ユーザーの取得に失敗しました");
}
}
JavaScriptcallApi の中でエラーを catch してログを出したあと、
再送出していないので、loadUsers の catch には届きません。
結果として、
・画面側は「成功したのか失敗したのか」分からない
・data が undefined のまま renderUsers を呼んで別のエラーになる
・ユーザーには何もエラー表示が出ない
といった状態になりがちです。
ここが重要です。
「catch したら必ず何かしら throw し直すべき」という話ではありませんが、
“ここで完全に処理を終えるのか?” “上に知らせる必要はないのか?” を必ず意識する必要があります。
再送出を忘れると、「静かに失敗する」コードになりやすいです。
エラーを「別の型」に変えて再送出する
生の Error → カスタムエラーに変換して投げ直す
再送出は、単に throw err するだけではなく、
「別のエラーにラップして投げ直す」 こともよくあります。
例えば、fetch のエラーや HTTP エラーを、ApiError というカスタムエラーに変換して上に渡したい場合です。
class ApiError extends Error {
constructor(message, status, body) {
super(message);
this.name = "ApiError";
this.status = status;
this.body = body;
}
}
async function callApi(url, options = {}) {
try {
const response = await fetch(url, options);
const body = await response.json().catch(() => null);
if (!response.ok) {
const message =
body?.error?.message || `HTTP error ${response.status}`;
throw new ApiError(message, response.status, body);
}
return body;
} catch (err) {
if (err instanceof ApiError) {
// すでに整形済みならそのまま再送出
throw err;
}
console.error("[callApi] ネットワークエラーなど:", err);
throw new ApiError("ネットワークエラーが発生しました", undefined, null);
}
}
JavaScriptここでは、
・HTTP エラー → ApiError に変換して throw
・その他のエラー(ネットワークなど) → ログを出してから、別の ApiError に変換して throw
という「変換+再送出」をしています。
呼び出し側は、こう扱えます。
async function loadUsers() {
try {
const body = await callApi("/api/users");
renderUsers(body.data);
} catch (err) {
if (err instanceof ApiError) {
showErrorMessage(err.message);
} else {
showErrorMessage("予期しないエラーが発生しました");
}
}
}
JavaScriptここが重要です。
再送出は、「そのまま上に投げる」だけでなく、
「自分のレイヤーにふさわしいエラー型に変換してから上に渡す」ためにも使われます。
これによって、上のレイヤーは「扱いやすいエラー」だけを相手にすればよくなります。
「ここでは処理する」「ここでは再送出する」の線引き
責務ごとにレイヤーを分けて考える
エラー再送出を設計するときに大事なのは、
「この関数の責務はどこまでか?」をはっきりさせることです。
例えば、こんなレイヤー分けを考えます。
通信レイヤー(callApi など)
→ fetch、ステータスチェック、ログ、カスタムエラーへの変換
→ ここでは「ユーザー向け表示」はしない
→ エラーは再送出して、上に判断を任せる
アプリケーションレイヤー(loadUsers など)
→ どの API を呼ぶか、結果をどう UI に反映するか
→ ここでユーザー向けメッセージを決める
→ 必要ならさらに上(グローバルハンドラ)に再送出することも
グローバルレイヤー(最上位のエラーハンドラ)
→ 想定外のエラーをまとめてログに送る、エラーページに飛ばすなど
このとき、
通信レイヤーでは「再送出する」のが基本、
アプリケーションレイヤーでは「ここで完結させる」ことが多い、
というように線引きができます。
ここが重要です。
再送出するかどうかは、「このレイヤーで完結させるべきエラーか?」「上のレイヤーにも判断させたいか?」で決めます。
なんとなく throw し直すのではなく、“責務の境界” を意識して決めるのがポイントです。
非同期処理でよくある「再送出パターン」
パターン1:ログ+再送出
一番よく使うのがこれです。
async function someService() {
try {
// 何か非同期処理
} catch (err) {
logError("someService", err);
throw err; // 上に伝える
}
}
JavaScript「ここではログだけ取りたい。
でも、エラー自体は上に知らせたい」というときの定番です。
パターン2:変換+再送出
生のエラーをカスタムエラーに変換してから投げ直すパターンです。
async function someService() {
try {
// 何か非同期処理
} catch (err) {
if (err instanceof SomeCustomError) {
throw err;
}
throw new SomeCustomError("サービス内でのエラー", { cause: err });
}
}
JavaScriptこれにより、
上のレイヤーは SomeCustomError だけを相手にすればよくなります。
パターン3:一部だけ処理して、残りを再送出
例えば、「ローディング表示を消す」「一時的な状態をリセットする」など、
このレイヤーでしかできない後始末をしてから再送出するパターンです。
async function loadUsers() {
setLoading(true);
try {
const data = await callApi("/api/users");
renderUsers(data);
} catch (err) {
resetTemporaryState();
throw err; // さらに上のグローバルハンドラに任せる
} finally {
setLoading(false);
}
}
JavaScriptここが重要です。
再送出は、「何もせずに投げる」のではなく、
“このレイヤーでしかできないこと” をやってから、
“このレイヤーでは決めきれないこと” を上に渡すための道具 です。
初心者として「エラー再送出」で本当に押さえてほしいこと
catch の中でエラーを「握りつぶす」と、
上のレイヤーは失敗に気づけなくなる。
ログだけ出して終わり、は危険なことが多い。
再送出は、
「ここではログや共通処理だけして、最終判断は上に任せる」
というためのテクニック。throw err でそのまま投げ直すのも、
カスタムエラーに変換して投げ直すのも、どちらも「再送出」。
非同期処理では、
共通 API 関数(callApi など)でログ+変換+再送出を行い、
画面側の関数でユーザー向け表示を決める、
というレイヤー分けがとても相性がいい。
再送出するかどうかは、
「この関数の責務はどこまでか?」
「このエラーはここで完結させるべきか? 上にも知らせるべきか?」
という問いで決める。
ここが重要です。
コードを書いていて catch に入ったとき、
一度立ち止まって、
「このエラー、ここで終わらせていいのか? それとも、何かしてから上に投げ直すべきか?」
と自分に問いかけてみてください。
その問いに答える形で、
ログを取り、状態を片付け、ときにはカスタムエラーに変換し、
必要なら再送出する——
そうやって少しずつ、「エラーの流れをデザインできるコード」に近づいていきます。
