Java 逆引き集 | カスタム例外の作り方 - 明確なエラー伝搬

Java Java
スポンサーリンク

カスタム例外って何者か(「意味のある失敗」に名前を付ける)

カスタム例外は、「自分のアプリケーション固有の失敗」に、わかりやすい名前と情報を付けて伝搬するための仕組みです。 IOExceptionNullPointerException のような汎用的な例外だけだと、「何がビジネス的に失敗したのか」がコードから読み取りづらくなります。 そこで「在庫不足」「認可エラー」「不正な注文」など、ドメインに沿った例外クラスを自分で定義して、上位レイヤに「この種類の失敗が起きた」と明確に伝えるわけです。

一番基本的なカスタム例外の作り方

Exception を継承して作る(チェック例外)

まずは、チェック例外(throws が必要なタイプ)のカスタム例外から見てみます。

public class OutOfStockException extends Exception {

    public OutOfStockException(String message) {
        super(message);
    }
}
Java

Exception を継承したクラスを作り、コンストラクタでメッセージを親クラスに渡しています。 これだけで「在庫不足」を表す独自の例外クラスができあがります。

この例外を投げる側はこう書きます。

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

こうした名前がスタックトレースやログに現れるだけで、「どの種類の失敗が起きたのか」が一目で分かります。 汎用的な RuntimeExceptionException だけだと、「何がビジネス的に問題なのか」を追うのが難しくなります。

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の通信エラー」という技術的な失敗を、「決済処理に失敗した」というビジネス的な失敗として上位レイヤに伝搬できます。 レイヤごとに「どのレベルの意味に変換するか」を決めておくと、システム全体のエラー設計が整理されていきます。

まとめと小さな練習

カスタム例外は、「失敗に名前と文脈を与えて、明確に伝搬する」ための道具です。 ExceptionRuntimeException を継承してクラスを作り、メッセージや追加情報を持たせ、必要なレイヤで投げて・受け止めて・ラップし直す。

練習として、次のようなカスタム例外を自分で作ってみると感覚がつかめます。

「ログイン失敗」を表す LoginFailedException を作り、「ユーザー名」と「失敗理由」をフィールドに持たせる 「不正な入力」を表す InvalidInputException を作り、バリデーションメソッドから投げて、呼び出し側でユーザー向けメッセージに変換する

実際に「例外クラスを定義する」「投げる」「catch する」「ログに出す」を一通り体験すると、エラー伝搬を自分の言葉で設計できるようになっていきます。

タイトルとURLをコピーしました