カスタム例外って何者か(「意味のある失敗」に名前を付ける)
カスタム例外は、「自分のアプリケーション固有の失敗」に、わかりやすい名前と情報を付けて伝搬するための仕組みです。 IOException や NullPointerException のような汎用的な例外だけだと、「何がビジネス的に失敗したのか」がコードから読み取りづらくなります。 そこで「在庫不足」「認可エラー」「不正な注文」など、ドメインに沿った例外クラスを自分で定義して、上位レイヤに「この種類の失敗が起きた」と明確に伝えるわけです。
一番基本的なカスタム例外の作り方
Exception を継承して作る(チェック例外)
まずは、チェック例外(throws が必要なタイプ)のカスタム例外から見てみます。
public class OutOfStockException extends Exception {
public OutOfStockException(String message) {
super(message);
}
}
JavaException を継承したクラスを作り、コンストラクタでメッセージを親クラスに渡しています。 これだけで「在庫不足」を表す独自の例外クラスができあがります。
この例外を投げる側はこう書きます。
public class OrderService {
public void placeOrder(String item, int quantity) throws OutOfStockException {
int stock = 0; // 本当は在庫をどこかから取得する
if (quantity > stock) {
throw new OutOfStockException("在庫が足りません: " + item);
}
System.out.println("注文を受け付けました: " + item);
}
}
Java呼び出し側は、この例外を意識して扱うことになります。
public class Main {
public static void main(String[] args) {
OrderService service = new OrderService();
try {
service.placeOrder("Book", 10);
} catch (OutOfStockException e) {
System.out.println("注文失敗: " + e.getMessage());
}
}
}
Javaここで重要なのは、「メソッドのシグネチャに throws OutOfStockException が現れることで、このメソッドは在庫不足という失敗を起こしうる」と、APIレイヤで明示されることです。
RuntimeException を継承するカスタム例外(実行時例外)
チェック例外か実行時例外かを選ぶ
もう一つのパターンが、RuntimeException を継承するカスタム例外です。 これは「呼び出し側に必ずしも強制的に扱わせたいわけではないが、意味のある失敗として伝えたい」場面で使われます。
public class InvalidOrderException extends RuntimeException {
public InvalidOrderException(String message) {
super(message);
}
}
Javaこの例外を投げるメソッドは、throws を書かなくてもコンパイルできます。
public class OrderValidator {
public void validate(int quantity) {
if (quantity <= 0) {
throw new InvalidOrderException("数量は1以上でなければなりません: " + quantity);
}
}
}
Java呼び出し側は、必要に応じて try / catch で受け止めます。
public class Main {
public static void main(String[] args) {
OrderValidator validator = new OrderValidator();
try {
validator.validate(0);
System.out.println("検証OK");
} catch (InvalidOrderException e) {
System.out.println("検証エラー: " + e.getMessage());
}
}
}
Javaチェック例外にするか実行時例外にするかは、「呼び出し側に必ず対応を強制したいか」「失敗がビジネス的に通常の分岐なのか、それともプログラムの誤りに近いのか」という観点で決めるとよいです。
カスタム例外に「追加情報」を持たせる
フィールドを持たせて、失敗の文脈を伝える
カスタム例外の強みは、「ただメッセージを持つだけでなく、失敗に関する追加情報をフィールドとして持てる」ことです。
public class OutOfStockException extends Exception {
private final String itemCode;
private final int requested;
private final int available;
public OutOfStockException(String itemCode, int requested, int available) {
super("在庫不足: item=" + itemCode + ", requested=" + requested + ", available=" + available);
this.itemCode = itemCode;
this.requested = requested;
this.available = available;
}
public String getItemCode() { return itemCode; }
public int getRequested() { return requested; }
public int getAvailable() { return available; }
}
Java投げる側は、具体的な情報を詰めて投げます。
if (quantity > stock) {
throw new OutOfStockException(itemCode, quantity, stock);
}
Java受け取る側は、メッセージだけでなく、フィールドから詳細な情報を取り出せます。
catch (OutOfStockException e) {
System.out.println("注文失敗: " + e.getMessage());
System.out.println("商品コード: " + e.getItemCode());
System.out.println("要求数量: " + e.getRequested());
System.out.println("在庫数量: " + e.getAvailable());
}
Javaログに詳細を残したり、管理画面に「どの商品がどれだけ不足しているか」を表示したりする場面で、こうした追加情報が非常に役立ちます。 「この失敗を調べるときに、何が分かっていてほしいか」を逆算して、フィールドを設計するとよいです。
明確なエラー伝搬としてのカスタム例外設計
「何が失敗したのか」を名前で伝える
カスタム例外の一番の価値は、「失敗の種類を名前で表現できる」ことです。
OutOfStockException InvalidOrderException UnauthorizedAccessException PaymentFailedException
こうした名前がスタックトレースやログに現れるだけで、「どの種類の失敗が起きたのか」が一目で分かります。 汎用的な RuntimeException や Exception だけだと、「何がビジネス的に問題なのか」を追うのが難しくなります。
APIレイヤでは、「このメソッドはどんな種類の失敗を起こしうるか」を、カスタム例外の型として明示することで、呼び出し側にとっても設計がしやすくなります。
レイヤごとに「どこまでラップするか」を決める
実務では、下位レイヤ(DBアクセス、外部APIなど)で発生した例外を、そのまま上に投げるのではなく、「ドメインに沿ったカスタム例外にラップして投げ直す」ことがよくあります。
public class PaymentService {
public void pay(int amount) throws PaymentFailedException {
try {
callPaymentGateway(amount);
} catch (IOException e) {
throw new PaymentFailedException("決済処理に失敗しました", e);
}
}
}
Javaカスタム例外のコンストラクタで「原因となった例外」を受け取り、super(message, cause) として渡すことで、元の技術的な例外もスタックトレースに残せます。
public class PaymentFailedException extends Exception {
public PaymentFailedException(String message, Throwable cause) {
super(message, cause);
}
}
Javaこうすることで、「外部APIの通信エラー」という技術的な失敗を、「決済処理に失敗した」というビジネス的な失敗として上位レイヤに伝搬できます。 レイヤごとに「どのレベルの意味に変換するか」を決めておくと、システム全体のエラー設計が整理されていきます。
まとめと小さな練習
カスタム例外は、「失敗に名前と文脈を与えて、明確に伝搬する」ための道具です。 Exception や RuntimeException を継承してクラスを作り、メッセージや追加情報を持たせ、必要なレイヤで投げて・受け止めて・ラップし直す。
練習として、次のようなカスタム例外を自分で作ってみると感覚がつかめます。
「ログイン失敗」を表す LoginFailedException を作り、「ユーザー名」と「失敗理由」をフィールドに持たせる 「不正な入力」を表す InvalidInputException を作り、バリデーションメソッドから投げて、呼び出し側でユーザー向けメッセージに変換する
実際に「例外クラスを定義する」「投げる」「catch する」「ログに出す」を一通り体験すると、エラー伝搬を自分の言葉で設計できるようになっていきます。
