カスタムエラーを一言でいうと
カスタムエラーは、
「自分のアプリ専用の“名前付きエラー型”を作って、エラーの意味をはっきりさせる仕組み」 です。
ただの Error だけだと、
「何が原因で」「どのレイヤーで」「どう扱うべきエラーなのか」が分かりにくくなります。
そこで、例えば
ValidationError(入力チェックの失敗)ApiError(API 通信は成功したが、サーバーがエラーを返した)AuthError(認証・ログイン関連のエラー)
のように、種類ごとにクラスを作って名前を付けることで、
catch した側が「これはどう扱うべきエラーか?」を判断しやすくなります。
ここが重要です。
カスタムエラーは、
「エラーをオシャレにするため」ではなく、
「エラーの“意味”をコードに刻み込むための道具」 です。
非同期処理(fetch など)と組み合わせると、その威力が一気に分かりやすくなります。
まずは一番シンプルなカスタムエラークラス
Error を継承して自分のエラー型を作る
JavaScript では、class を使って Error を継承することで、
自分専用のエラー型を作れます。
例えば「バリデーションエラー」を表すクラスを作ってみましょう。
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
JavaScriptポイントは二つです。
extends Error で Error を継承していること。this.name = "ValidationError"; でエラー名を分かりやすくしていること。
これで、次のように使えます。
function validateUserName(name) {
if (!name) {
throw new ValidationError("ユーザー名は必須です");
}
if (name.length < 3) {
throw new ValidationError("ユーザー名は3文字以上にしてください");
}
}
try {
validateUserName("");
} catch (err) {
console.log(err.name); // "ValidationError"
console.log(err.message); // "ユーザー名は必須です"
}
JavaScript普通の Error だと name は "Error" ですが、
カスタムエラーなら "ValidationError" になります。
ここが重要です。
エラーの name が意味のあるものになるだけで、
ログを見たときに「何系のエラーか」が一瞬で分かるようになります。
これがカスタムエラーの一番小さな、でも強力な効果です。
非同期処理(fetch)とカスタムエラーの組み合わせ
API エラー用のカスタムエラーを作る
非同期処理、とくに fetch / API 通信では、
「HTTP レベルでは成功しているけど、アプリ的にはエラー」という状況がよくあります。
例えば、ステータスコードは 400 や 422 で、
レスポンスボディに「バリデーションエラーの詳細」が入っているようなケースです。
こういうときに使えるのが ApiError のようなカスタムエラーです。
class ApiError extends Error {
constructor(message, status, body) {
super(message);
this.name = "ApiError";
this.status = status;
this.body = body;
}
}
JavaScriptこのエラーは、
「サーバーが返してきたステータスコード」や
「レスポンスボディ(エラー詳細)」を一緒に持てるようにしています。
fetch をラップして ApiError を投げる
この ApiError を使って、共通の API 呼び出し関数を作ってみます。
async function callApi(url, options = {}) {
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...(options.headers || {}),
},
...options,
});
let body = null;
try {
body = await response.json();
} catch (e) {
// JSON でない場合は body は null のままでもよい
}
if (!response.ok) {
const message =
body?.error?.message ||
`HTTP エラー: ${response.status}`;
throw new ApiError(message, response.status, body);
}
return body;
}
JavaScriptここでやっていることはこうです。
fetch でレスポンスを取得する。
JSON をパースして body に入れる(失敗したら null のまま)。response.ok が false(ステータス 200〜299 以外)なら、ApiError を投げる。
このとき、ApiError の中に
メッセージ(ユーザー向け or ログ向け)
ステータスコード
レスポンスボディ(エラー詳細)
を詰め込んでいます。
これで、呼び出し側は「ただの Error」ではなく、
「API 由来のエラー(ApiError)」として扱えるようになります。
呼び出し側で ApiError を判別する
呼び出し側のコードは、こう書けます。
async function loadUsers() {
try {
const body = await callApi("/api/users");
renderUsers(body.data);
} catch (err) {
if (err instanceof ApiError) {
console.error("API エラー:", err.status, err.message);
alert(err.message);
} else {
console.error("想定外のエラー:", err);
alert("予期しないエラーが発生しました");
}
}
}
JavaScripterr instanceof ApiError で、
「これは API 由来のエラーか?」を判定できます。
API エラーなら、
ステータスコードやサーバーからのメッセージを使ってユーザーに伝える。
それ以外(コードのバグなど)は、
「想定外のエラー」として別扱いにする。
ここが重要です。
カスタムエラーを使うと、「どのエラーはユーザーに見せるべきか」「どのエラーは開発者向けのバグか」をコードで分けられるようになります。
これは、非同期処理のエラー設計でめちゃくちゃ大事なポイントです。
カスタムエラーで「エラーの種類」を表現する
認証エラー・権限エラーなどをクラスで分ける
API を使っていると、
「ログインしていない」「権限がない」といったエラーもよく出てきます。
これらを AuthError や PermissionError として表現すると、
エラー処理がかなり分かりやすくなります。
class AuthError extends Error {
constructor(message = "認証エラーです") {
super(message);
this.name = "AuthError";
}
}
class PermissionError extends Error {
constructor(message = "権限がありません") {
super(message);
this.name = "PermissionError";
}
}
JavaScriptこれを callApi の中で使ってみます。
async function callApi(url, options = {}) {
const response = await fetch(url, options);
if (response.status === 401) {
throw new AuthError("ログインが必要です");
}
if (response.status === 403) {
throw new PermissionError("この操作を行う権限がありません");
}
if (!response.ok) {
throw new ApiError("HTTP エラー: " + response.status, response.status);
}
return await response.json();
}
JavaScript呼び出し側で「種類ごと」に分岐する
呼び出し側は、こう書けます。
async function loadProfile() {
try {
const data = await callApi("/api/profile");
renderProfile(data);
} catch (err) {
if (err instanceof AuthError) {
alert(err.message);
redirectToLogin();
} else if (err instanceof PermissionError) {
alert(err.message);
} else if (err instanceof ApiError) {
alert("サーバーでエラーが発生しました。時間をおいて再度お試しください。");
} else {
alert("予期しないエラーが発生しました。");
console.error(err);
}
}
}
JavaScriptここで注目してほしいのは、
AuthError → ログイン画面へ誘導
PermissionError → 権限なしメッセージを表示
ApiError → 汎用的なサーバーエラーとして扱う
その他 → バグの可能性が高いのでログをしっかり出す
というように、
「エラーの種類ごとに、ユーザー体験を変えられている」 ことです。
ここが重要です。
カスタムエラーは、
「エラーの種類を if 文の文字列比較ではなく、“型” として表現する」 ための仕組みです。instanceof で分岐できるようになると、エラー処理のロジックがかなり読みやすくなります。
カスタムエラーに「追加情報」を持たせる
単なるメッセージ以上の情報を持てる
カスタムエラーの強みは、message だけでなく、好きなプロパティを持たせられることです。
例えば、フォームのバリデーションエラーで、
「どのフィールドが」「どんな理由で」エラーなのかを持たせたいとします。
class FormValidationError extends Error {
constructor(message, fieldErrors) {
super(message);
this.name = "FormValidationError";
this.fieldErrors = fieldErrors; // { fieldName: "エラーメッセージ", ... }
}
}
JavaScriptこれを使って、こんな関数を書けます。
function validateForm(form) {
const errors = {};
if (!form.email) {
errors.email = "メールアドレスは必須です";
}
if (!form.password || form.password.length < 8) {
errors.password = "パスワードは8文字以上にしてください";
}
if (Object.keys(errors).length > 0) {
throw new FormValidationError("入力内容に誤りがあります", errors);
}
}
JavaScriptcatch 側で UI に反映する
呼び出し側では、こう扱えます。
async function handleSubmit(form) {
try {
validateForm(form);
await callApi("/api/register", {
method: "POST",
body: JSON.stringify(form),
});
alert("登録が完了しました");
} catch (err) {
if (err instanceof FormValidationError) {
showFieldErrors(err.fieldErrors);
} else if (err instanceof ApiError) {
alert(err.message);
} else {
alert("予期しないエラーが発生しました");
console.error(err);
}
}
}
JavaScriptFormValidationError の場合だけ、err.fieldErrors を使ってフォームの各フィールドにエラーメッセージを表示する、
という UI が実現できます。
ここが重要です。
カスタムエラーは、「ただのメッセージ」ではなく、「UI やロジックに必要な追加情報」を運ぶコンテナにもなれる ということです。
これができると、非同期処理とフォーム処理が絡んだ複雑な画面でも、エラーの流れをきれいに設計できます。
カスタムエラーと「エラー設計」の関係
どんなエラー型を用意するかは「設計」の話
カスタムエラーを使い始めると、
「どんな種類のエラーをクラスとして切り出すか?」
という設計の話になってきます。
よくある分け方の例としては、次のようなものがあります。
ドメイン(業務)エラー
例: InsufficientBalanceError, StockShortageError など。
ビジネスルールに違反したときのエラー。
アプリケーションエラー
例: ValidationError, FormValidationError, ApiError など。
入力や API の使い方が間違っているときのエラー。
システムエラー
例: 予期しない例外、バグ、外部サービスの障害など。
基本的にはユーザーに詳細を見せず、ログに残す。
全部を一気にやる必要はありませんが、
「自分のアプリでは、どんなエラーの種類があるのか?」を言葉にしてみると、
どこでカスタムエラーを使うべきかが見えてきます。
非同期処理の中で「どこで何を投げるか」を決める
例えば、fetch を使った非同期処理の流れを考えると、
ネットワークレベルの失敗(タイムアウト・接続エラーなど)
→ そのまま Error か、NetworkError のようなカスタムエラーにする。
HTTP ステータスエラー(400, 401, 403, 404, 500 など)
→ ApiError, AuthError, PermissionError などに変換する。
レスポンスボディの中身としての業務エラー(例: 残高不足)
→ BusinessError のようなカスタムエラーに変換する。
といった「変換ポイント」を決めておくと、
上のレイヤーでは「Error の種類だけを見て判断する」ことができます。
ここが重要です。
カスタムエラーは、「非同期処理の中で発生したバラバラなエラーを、“意味のある型” に揃えて上に渡すための変換レイヤー」 として使うと、真価を発揮します。
どこで何を投げるかを決めること自体が、エラー設計そのものです。
初心者として「カスタムエラー」で本当に押さえてほしいこと
カスタムエラーは、class XxxError extends Error で作る。this.name を設定しておくと、ログやデバッグで「何のエラーか」が分かりやすくなる。
非同期処理(fetch など)では、
「ただの Error を投げる」のではなく、ApiError, AuthError, ValidationError など、意味のあるエラー型に変換して投げると、
catch 側での分岐がシンプルになる。
instanceof を使って、
「このエラーはどの種類か?」を判定し、
種類ごとにユーザーへの見せ方や遷移先を変えられる。
カスタムエラーには、message だけでなく、status, body, fieldErrors など、
UI やロジックに必要な追加情報を持たせることができる。
ここが重要です。
エラーを「ただの失敗」ではなく、「意味のある出来事」として扱うために、カスタムエラーを使う。
そのとき必ず自分に問いかけてほしいのは、
「このエラーは、何を表していて、どんなふるまいをしてほしいのか?」ということです。
その問いに名前(クラス名)を与えてあげる。
それが、カスタムエラーを使うという行為の本質です。
小さなところからでいいので、ValidationError や ApiError など、一つずつ導入してみてください。
エラー処理が「ただの後始末」から、「アプリの振る舞いをデザインする場所」に変わっていく感覚が、きっとつかめます。
