Javaの独自例外(カスタム例外)とは、Exception または RuntimeException を継承して自分で定義する例外クラスのことで、public class 名前Exception extends Exception { ... } と書き、コンストラクタで super(message, cause) を呼べば作れます。 「在庫が足りない」「数量を読み取れない」のように、アプリケーション固有の出来事に名前を付けられるのが利点です。

この記事では、4種類のコンストラクタをそろえたひな形を Exception 版と RuntimeException 版の両方で用意しました。さらに、原因(cause)を渡した場合と渡さなかった場合のスタックトレースを並べ、Caused by が表示される様子と消える様子を再現します。エラーコードを持つ例外、getCause でたどる例、どちらを継承するかの判断表まで、すべて実行結果つきで紹介します。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。各ファイルを同じフォルダに保存し、javac -d classes *.java でコンパイル、java -cp classes クラス名 で実行しています。

独自例外の早見表

作り方の手順

手順 すること 書き方
1 親クラスを決めて継承する extends Exception(検査例外)または extends RuntimeException(非検査例外)
2 クラス名の末尾を Exception にする StockShortageException など、出来事がわかる名前
3 コンストラクタを用意する super() / super(message) / super(message, cause) / super(cause)
4 serialVersionUID を書く private static final long serialVersionUID = 1L;
5 投げる throw new StockShortageException("在庫が足りません");
6 別の例外を包むときは原因を渡す throw new StockShortageException("...", e);

4種類のコンストラクタと、getMessage・getCause の結果

コンストラクタ 用途 getMessage() getCause()
() メッセージなしで投げる null null
(String message) 自分で見つけた問題を知らせる 渡したメッセージ null
(String message, Throwable cause) 別の例外を包む(おすすめ) 渡したメッセージ 渡した例外
(Throwable cause) メッセージを付けずに包む 原因の toString() の結果 渡した例外

どちらを継承するかの目安

継承するクラス 種類 throws や try-catch 向いている場面
Exception 検査例外 必須(ないとコンパイルエラー) 呼び出し側に対応を必ず書いてほしいとき
RuntimeException 非検査例外 任意 呼び出し側ではその場で対応しにくく、上の層でまとめて扱うとき

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java 独自例外の作り方|extends Exceptionと原因のラップ」

コピペで使える独自例外のひな形

Exception 版(検査例外)

4種類のコンストラクタをそろえた形です。クラス名と説明のコメントを書き換えれば、そのまま使えます。

/** 在庫が足りないことを表す独自例外(検査例外) */
public class StockShortageException extends Exception {

    private static final long serialVersionUID = 1L;

    /** 1. 引数なし */
    public StockShortageException() {
        super();
    }

    /** 2. メッセージ */
    public StockShortageException(String message) {
        super(message);
    }

    /** 3. メッセージと原因 */
    public StockShortageException(String message, Throwable cause) {
        super(message, cause);
    }

    /** 4. 原因だけ */
    public StockShortageException(Throwable cause) {
        super(cause);
    }
}

RuntimeException 版(非検査例外)

違いは extends の後ろだけです。

/** 注文の内容が正しくないことを表す独自例外(非検査例外) */
public class InvalidOrderException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    /** 1. 引数なし */
    public InvalidOrderException() {
        super();
    }

    /** 2. メッセージ */
    public InvalidOrderException(String message) {
        super(message);
    }

    /** 3. メッセージと原因 */
    public InvalidOrderException(String message, Throwable cause) {
        super(message, cause);
    }

    /** 4. 原因だけ */
    public InvalidOrderException(Throwable cause) {
        super(cause);
    }
}

ひな形を使ってみる

2つの例外を投げて受け取り、4種類のコンストラクタで getMessage() と getCause() がどうなるかを確かめます。

public class TemplateDemo {

    // 検査例外を投げるメソッドは throws が必要
    static void reserve(int stock, int quantity) throws StockShortageException {
        if (quantity > stock) {
            throw new StockShortageException("在庫が足りません: 在庫=" + stock + ", 注文数=" + quantity);
        }
        System.out.println("引き当て完了: " + quantity + "個");
    }

    // 非検査例外を投げるメソッドは throws が不要
    static void validate(int quantity) {
        if (quantity <= 0) {
            throw new InvalidOrderException("注文数は1以上で指定してください: " + quantity);
        }
    }

    public static void main(String[] args) {
        try {
            reserve(10, 3);
            reserve(10, 30);
        } catch (StockShortageException e) {
            System.out.println("catch: " + e.getMessage());
        }

        try {
            validate(0);
        } catch (InvalidOrderException e) {
            System.out.println("catch: " + e.getMessage());
        }

        // 4種類のコンストラクタで getMessage と getCause がどうなるか
        Throwable cause = new IllegalStateException("元の例外");
        StockShortageException[] list = {
            new StockShortageException(),
            new StockShortageException("メッセージ"),
            new StockShortageException("メッセージ", cause),
            new StockShortageException(cause),
        };
        for (StockShortageException e : list) {
            System.out.println("getMessage=" + e.getMessage() + " / getCause=" + e.getCause());
        }
    }
}

実行結果です。

引き当て完了: 3個
catch: 在庫が足りません: 在庫=10, 注文数=30
catch: 注文数は1以上で指定してください: 0
getMessage=null / getCause=null
getMessage=メッセージ / getCause=null
getMessage=メッセージ / getCause=java.lang.IllegalStateException: 元の例外
getMessage=java.lang.IllegalStateException: 元の例外 / getCause=java.lang.IllegalStateException: 元の例外

最後の4行が、コンストラクタごとの違いです。

  • 引数なしは、メッセージも原因も null です。
  • 原因だけを渡すと、メッセージには原因の toString() の結果(クラス名とメッセージ)が入ります。 Java 21 の API ドキュメントにも、Throwable(Throwable cause) の詳細メッセージは (cause==null ? null : cause.toString()) になると書かれています。
  • メッセージと原因の両方を渡す形では、原因のメッセージは自動では取り込まれません。必要な情報は、自分のメッセージに書きます。

4つすべて必要か

コンストラクタは親クラスから引き継がれないため、使いたい形は自分で書きます。必ず4つそろえる必要はありません。

用意するコンストラクタ 判断
(String message) と (String message, Throwable cause) この2つがあれば、ほとんどの場面をカバーできる
(Throwable cause) メッセージを付け足す必要がないときに便利
() メッセージなしで投げる場面がなければ省いてよい

検査例外と非検査例外の違いを動かして確かめる

Exception を継承した独自例外は検査例外になり、投げるメソッドに throws を書くか、try-catch で受けるかのどちらかが必要です。どちらもないとコンパイルが止まります。

public class NoThrows {

    static void reserve(int stock, int quantity) {
        if (quantity > stock) {
            throw new StockShortageException("在庫が足りません");
        }
    }

    public static void main(String[] args) {
        reserve(10, 30);
    }
}
NoThrows.java:5: error: unreported exception StockShortageException; must be caught or declared to be thrown
            throw new StockShortageException("在庫が足りません");
            ^
1 error

メソッドの宣言を static void reserve(int stock, int quantity) throws StockShortageException に直し、呼び出す側を try-catch で囲むと、先ほどの TemplateDemo のとおり動きます。RuntimeException を継承した InvalidOrderException は、throws がなくてもコンパイルできます。

どちらを継承するかの判断表

判断の観点 Exception(検査例外) RuntimeException(非検査例外)
呼び出し側がその場で対応できるか できる(別の候補を出す、再入力を求めるなど) しにくい(設定の誤り、プログラムの誤りなど)
コンパイラに対応漏れを見つけてほしいか はい いいえ
ラムダ式の中で投げるか forEach のラムダ式の中で投げると unreported exception のコンパイルエラーになる そのまま投げられる
メソッドの宣言 throws が増える 変わらない
途中の層を素通りさせたいか 各層に throws が必要 何も書かなくてよい

迷ったときの目安は次のとおりです。

  • 「呼び出し側が必ず対応を書くべき出来事」なら Exception。
  • 「上の層でまとめてログに出し、利用者に知らせる」形なら RuntimeException。
  • チームやフレームワークに決まりがあれば、それに合わせるのがいちばんです。

原因(cause)をラップする

下の層で起きた例外を、意味のわかる独自例外に包み直して投げることを「ラップ」と呼びます。このとき、コンストラクタの第2引数に元の例外を渡すかどうかで、スタックトレースに残る情報が大きく変わります。

まず、2つの引数を持つ独自例外を用意します。

/** 数量の文字列を読み取れないことを表す独自例外 */
public class QuantityParseException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public QuantityParseException(String message) {
        super(message);
    }

    public QuantityParseException(String message, Throwable cause) {
        super(message, cause);
    }
}

cause を渡した場合

public class WithCause {

    static int parseQuantity(String text) {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            // 第2引数に元の例外を渡す
            throw new QuantityParseException("数量を読み取れません: " + text, e);
        }
    }

    public static void main(String[] args) {
        int quantity = parseQuantity("12個");
        System.out.println(quantity);
    }
}
Exception in thread "main" QuantityParseException: 数量を読み取れません: 12個
    at WithCause.parseQuantity(WithCause.java:8)
    at WithCause.main(WithCause.java:13)
Caused by: java.lang.NumberFormatException: For input string: "12個"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)
    at java.base/java.lang.Integer.parseInt(Integer.java:778)
    at WithCause.parseQuantity(WithCause.java:5)
    ... 1 more

cause を渡さなかった場合

違いは throw の行だけです。

public class WithoutCause {

    static int parseQuantity(String text) {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            // 元の例外を渡していない
            throw new QuantityParseException("数量を読み取れません: " + text);
        }
    }

    public static void main(String[] args) {
        int quantity = parseQuantity("12個");
        System.out.println(quantity);
    }
}
Exception in thread "main" QuantityParseException: 数量を読み取れません: 12個
    at WithoutCause.parseQuantity(WithoutCause.java:8)
    at WithoutCause.main(WithoutCause.java:13)

Caused by: 以下がまるごと消えました。 残っているのは「8行目で独自例外を作った」という情報だけで、もとの NumberFormatException が Integer.parseInt のどこで起きたのかは、もう追えません。

比べる点 cause を渡した cause を渡さなかった
Caused by: の表示 あり なし
元の例外のクラス名 java.lang.NumberFormatException とわかる わからない
元の例外が起きた行 WithCause.java:5 とわかる わからない
getCause() 元の例外 null

Caused by の読み方

表示 意味
1行目(Exception in thread "main" QuantityParseException: ...) 最後に投げられた例外(いちばん外側)
その下の at ... その例外が作られた場所。上の行ほど新しい呼び出し
Caused by: ... その例外の原因になった例外。下にある Caused by ほど根本の原因に近い
... 1 more 残りの行が、外側の例外のスタックトレースの下から1行分と同じなので省略した、という印

API ドキュメントでも、... を含む行は「残りの部分が、この例外を原因に持つ外側の例外のスタックトレースの下から数えた行数分と一致する」ことを示すと説明されています。調査のときは、いちばん下の Caused by から読むと、最初に起きた出来事にたどり着けます。

ラップするときの書き方(直す前と後)

直す前 直した後 理由
throw new XxxException("読み取れません"); throw new XxxException("読み取れません", e); 原因が残る
throw new XxxException(e.getMessage()); throw new XxxException("数量を読み取れません: " + text, e); メッセージだけでは元のクラス名と行がわからない
e.printStackTrace(); throw new XxxException("..."); throw new XxxException("...", e); 原因を渡しておけば、出力は受け取る側の1か所にまとめられる

getCause で原因をたどる

例外が何段にも包まれているとき、getCause() を繰り返し呼ぶと1段ずつさかのぼれます。3段に重なった例で確かめます。

public class CauseChain {

    static void load() {
        try {
            Integer.parseInt("12個");
        } catch (NumberFormatException e) {
            throw new QuantityParseException("数量を読み取れません", e);
        }
    }

    static void process() throws StockShortageException {
        try {
            load();
        } catch (QuantityParseException e) {
            throw new StockShortageException("在庫の確認を完了できません", e);
        }
    }

    /** いちばん奥の原因を返す */
    static Throwable rootCause(Throwable t) {
        Throwable current = t;
        while (current.getCause() != null) {
            current = current.getCause();
        }
        return current;
    }

    public static void main(String[] args) {
        try {
            process();
        } catch (StockShortageException e) {
            // getCause で1段ずつたどる
            int depth = 0;
            for (Throwable t = e; t != null; t = t.getCause()) {
                System.out.println(depth++ + ": " + t.getClass().getName() + " / " + t.getMessage());
            }
            System.out.println("根本の原因: " + rootCause(e));

            // 原因の型で分ける
            if (e.getCause() instanceof QuantityParseException q) {
                System.out.println("原因は数量の読み取り: " + q.getMessage());
            }

            System.out.println("--- printStackTrace ---");
            e.printStackTrace(System.out);
        }
    }
}
0: StockShortageException / 在庫の確認を完了できません
1: QuantityParseException / 数量を読み取れません
2: java.lang.NumberFormatException / For input string: "12個"
根本の原因: java.lang.NumberFormatException: For input string: "12個"
原因は数量の読み取り: 数量を読み取れません
--- printStackTrace ---
StockShortageException: 在庫の確認を完了できません
    at CauseChain.process(CauseChain.java:15)
    at CauseChain.main(CauseChain.java:30)
Caused by: QuantityParseException: 数量を読み取れません
    at CauseChain.load(CauseChain.java:7)
    at CauseChain.process(CauseChain.java:13)
    ... 1 more
Caused by: java.lang.NumberFormatException: For input string: "12個"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:662)
    at java.base/java.lang.Integer.parseInt(Integer.java:778)
    at CauseChain.load(CauseChain.java:5)
    ... 2 more
  • for (Throwable t = e; t != null; t = t.getCause()) の形で、外側から順にたどれます。
  • rootCause は、getCause() が null になるまで進んで、いちばん奥の例外を返します。
  • e.getCause() instanceof QuantityParseException q のように、原因の型で処理を分けることもできます。
  • printStackTrace では Caused by が2つ並び、下のものほど根本の原因です。... 2 more は、外側と同じ2行が省略されたことを表します。

エラーコードを持つ独自例外

例外にフィールドを足せば、メッセージ以外の情報も運べます。よく使われるのがエラーコードです。コードの一覧を enum にしておくと、打ち間違いをコンパイル時に見つけられ、switch で漏れなく分けられます。

/** エラーコードの一覧 */
public enum ErrorCode {
    STOCK_SHORTAGE("E1001", "在庫が足りません"),
    ITEM_NOT_FOUND("E1002", "商品が見つかりません"),
    INVALID_QUANTITY("E2001", "数量の指定が正しくありません"),
    UNEXPECTED("E9999", "予期しないエラーです");

    private final String code;
    private final String defaultMessage;

    ErrorCode(String code, String defaultMessage) {
        this.code = code;
        this.defaultMessage = defaultMessage;
    }

    public String code() {
        return code;
    }

    public String defaultMessage() {
        return defaultMessage;
    }
}
/** エラーコードを持つ独自例外 */
public class AppException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    private final ErrorCode errorCode;

    public AppException(ErrorCode errorCode) {
        super(errorCode.defaultMessage());
        this.errorCode = errorCode;
    }

    public AppException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public AppException(ErrorCode errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }

    /** ログに出したときにコードも見えるようにする */
    @Override
    public String toString() {
        return getClass().getName() + "[" + errorCode.code() + "]: " + getMessage();
    }
}

使う側のコードです。

import java.util.Map;

public class ErrorCodeDemo {

    static final Map<String, Integer> STOCK = Map.of("A-100", 5, "B-200", 0);

    static void order(String itemId, String quantityText) {
        int quantity;
        try {
            quantity = Integer.parseInt(quantityText);
        } catch (NumberFormatException e) {
            throw new AppException(ErrorCode.INVALID_QUANTITY, "数量を読み取れません: " + quantityText, e);
        }
        Integer stock = STOCK.get(itemId);
        if (stock == null) {
            throw new AppException(ErrorCode.ITEM_NOT_FOUND, "商品が見つかりません: " + itemId);
        }
        if (quantity > stock) {
            throw new AppException(ErrorCode.STOCK_SHORTAGE);
        }
        System.out.println("注文を受け付けました: " + itemId + " x " + quantity);
    }

    public static void main(String[] args) {
        String[][] orders = { {"A-100", "2"}, {"A-100", "9"}, {"Z-999", "1"}, {"B-200", "二"} };
        for (String[] o : orders) {
            try {
                order(o[0], o[1]);
            } catch (AppException e) {
                // コードで処理を分ける
                String action = switch (e.getErrorCode()) {
                    case STOCK_SHORTAGE -> "入荷待ちを案内";
                    case ITEM_NOT_FOUND, INVALID_QUANTITY -> "入力の見直しを案内";
                    case UNEXPECTED -> "担当者に連絡";
                };
                System.out.println(e.getErrorCode().code() + " / " + e.getMessage()
                        + " / 対応: " + action + " / cause=" + e.getCause());
            }
        }
        System.out.println(new AppException(ErrorCode.UNEXPECTED));
    }
}
注文を受け付けました: A-100 x 2
E1001 / 在庫が足りません / 対応: 入荷待ちを案内 / cause=null
E1002 / 商品が見つかりません: Z-999 / 対応: 入力の見直しを案内 / cause=null
E2001 / 数量を読み取れません: 二 / 対応: 入力の見直しを案内 / cause=java.lang.NumberFormatException: For input string: "二"
AppException[E9999]: 予期しないエラーです
  • e.getErrorCode() で受け取り側の処理を分けられます。 メッセージの文字列を見て判断するより、文言の変更に強くなります。
  • コードごとの既定のメッセージを enum に持たせたので、new AppException(ErrorCode.STOCK_SHORTAGE) だけでもメッセージが入ります。
  • 原因つきのコンストラクタを用意したので、数量の変換エラーは cause に NumberFormatException が残っています。
  • toString() を上書きしたので、最後の行のようにログにコードが出ます。
持たせる情報 例 持たせ方
エラーコード E1001 enum のフィールド
対象の ID 商品 ID、注文番号 private final のフィールドとゲッター
数値 在庫数、注文数 同上

フィールドは final にしてコンストラクタで設定すると、作った後に値が変わらず、扱いやすくなります。

serialVersionUID を書く理由

Throwable は Serializable(オブジェクトをバイト列に変換できる印)を実装しているため、独自例外も直列化できるクラスになります。serialVersionUID を書かないクラスを javac -Xlint:serial でコンパイルすると、次の警告が出ます。

public class NoSerialException extends Exception {

    public NoSerialException(String message) {
        super(message);
    }
}
NoSerialException.java:1: warning: [serial] serializable class NoSerialException has no definition of serialVersionUID
public class NoSerialException extends Exception {
       ^
1 warning

private static final long serialVersionUID = 1L; を1行足すと、この警告は出なくなります。ひな形に入れてあるのはそのためです。

catch の順番で出るコンパイルエラー

独自例外は親クラスの型でも受け取れます。そのため、親クラスの catch を先に書くと、後ろの catch には届かなくなり、コンパイルが止まります。

public class CatchOrder {

    static void reserve() throws StockShortageException {
        throw new StockShortageException("在庫が足りません");
    }

    public static void main(String[] args) {
        try {
            reserve();
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
        } catch (StockShortageException e) {
            System.out.println("StockShortageException: " + e.getMessage());
        }
    }
}
CatchOrder.java:12: error: exception StockShortageException has already been caught
        } catch (StockShortageException e) {
          ^
1 error

catch は「子クラス(独自例外)を先、親クラス(Exception)を後」に並べます。

応用:スタックトレースなしの例外と initCause

スタックトレースを記録しない軽い例外

Throwable には、スタックトレースを記録するかどうかを選べる4引数の protected コンストラクタがあります(Java 7 で追加)。入力チェックのように、発生した場所よりも内容が大切な例外で使えます。

/** スタックトレースを記録しない軽い独自例外 */
public class LightException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    public LightException(String message) {
        // 第3引数: suppressed を使うか / 第4引数: スタックトレースを記録するか
        super(message, null, false, false);
    }

    public static void main(String[] args) {
        try {
            throw new LightException("入力が空です");
        } catch (LightException e) {
            System.out.println("getMessage: " + e.getMessage());
            System.out.println("スタックトレースの行数: " + e.getStackTrace().length);
            e.printStackTrace(System.out);
        }
    }
}
getMessage: 入力が空です
スタックトレースの行数: 0
LightException: 入力が空です

at ... の行が1つも出ていません。調査の手がかりが減るので、使う場面を絞るのがおすすめです。

原因を後から設定する initCause

原因を受け取るコンストラクタがない例外には、initCause で後から原因を設定できます。

public class InitCauseDemo {

    /** メッセージのコンストラクタしかない例外 */
    static class SimpleException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        SimpleException(String message) {
            super(message);
        }
    }

    public static void main(String[] args) {
        try {
            Integer.parseInt("x");
        } catch (NumberFormatException e) {
            SimpleException wrapped = new SimpleException("変換できません");
            wrapped.initCause(e); // 後から原因を設定する
            System.out.println("getCause: " + wrapped.getCause());
            try {
                wrapped.initCause(e); // 2回目
            } catch (IllegalStateException ex) {
                System.out.println(ex);
            }
        }
    }
}
getCause: java.lang.NumberFormatException: For input string: "x"
java.lang.IllegalStateException: Can't overwrite cause with java.lang.NumberFormatException: For input string: "x"

initCause を呼べるのは1回だけで、2回目は IllegalStateException になります。自分で作る例外なら、原因つきのコンストラクタを用意するほうが簡潔です。

つまずきやすい点と直し方

症状 原因 直し方
unreported exception XxxException; must be caught or declared to be thrown 検査例外を投げるのに throws も try-catch もない メソッドに throws を書くか、try-catch で受ける
exception XxxException has already been caught 親クラスの catch が先にある 子クラスの catch を先に書く
Caused by が出ず、元の例外がわからない ラップするときに cause を渡していない new XxxException(message, e) の形にする
[serial] serializable class ... has no definition of serialVersionUID serialVersionUID がない private static final long serialVersionUID = 1L; を足す
constructor XxxException in class XxxException cannot be applied to given types; その引数のコンストラクタを定義していない (String, Throwable) のコンストラクタを足す
getMessage() が原因のクラス名つきの文字列になる (Throwable cause) のコンストラクタを使った メッセージも渡す形にする
Can't overwrite cause with ... initCause を2回呼んだ、またはコンストラクタで原因を設定済み 原因はコンストラクタで1回だけ渡す

独自例外を作るときのチェックリスト

確認すること 目安
標準の例外で足りないか 引数の誤りは IllegalArgumentException、状態の誤りは IllegalStateException で表せることが多い
クラス名 末尾を Exception にし、出来事がわかる名前にする
親クラス 判断表で Exception か RuntimeException を選ぶ
コンストラクタ (String) と (String, Throwable) は用意する
メッセージ 何が、どの値で起きたかを書く(例:在庫=10, 注文数=30)
ラップ 必ず cause を渡す
種類を増やしすぎない 受け取り側で処理を分けたい単位で作る。細かい区別はエラーコードで表す

よくある質問

Q. extends Exception と extends RuntimeException は、どちらを選べばよいですか? A. 呼び出し側に対応を必ず書いてほしいなら Exception、上の層でまとめて扱うなら RuntimeException です。Exception を継承すると throws か try-catch がないとコンパイルエラーになり、対応漏れに気づけます。RuntimeException はメソッドの宣言を変えずに投げられます。

Q. コンストラクタは4種類すべて必要ですか? A. 必須ではありません。(String message) と (String message, Throwable cause) の2つがあれば、多くの場面で足ります。原因を渡せる形を1つは用意しておくと、ラップするときに Caused by を残せます。

Q. Caused by が表示されないのはなぜですか? A. 独自例外を作るときに、元の例外をコンストラクタに渡していないためです。throw new XxxException("メッセージ", e); のように第2引数で渡し、独自例外のコンストラクタでも super(message, cause) を呼びます。

Q. 独自例外にフィールドを追加してもよいですか? A. 追加できます。エラーコードや対象の ID を private final のフィールドに持たせ、ゲッターで取り出す形が扱いやすいです。受け取り側は、メッセージの文字列ではなくフィールドの値で処理を分けられます。

まとめ

  • 独自例外は extends Exception(検査例外)か extends RuntimeException(非検査例外)で作る。 クラス名の末尾は Exception。
  • コンストラクタは ()・(String)・(String, Throwable)・(Throwable) の4種類。 最低でも (String) と (String, Throwable) を用意する。
  • ラップするときは必ず cause を渡す。 渡さないと Caused by が消え、元の例外のクラス名も行も追えなくなる。
  • スタックトレースは、いちばん下の Caused by から読むと根本の原因にたどり着ける。
  • getCause() を繰り返すと、包まれた例外を1段ずつたどれる。
  • エラーコードは enum のフィールドとして持たせると、受け取り側が switch で分けやすい。
  • serialVersionUID を書き、catch は子クラスを先に並べる。

参考・出典

  • Java SE 21 API ドキュメント:java.lang.Throwable(https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java 独自例外の作り方|extends Exceptionと原因のラップ」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?