CodeGym /コース /JAVA 25 SELF /例外の連鎖(Exception Chaining)

例外の連鎖(Exception Chaining)

JAVA 25 SELF
レベル 24 , レッスン 2
使用可能

1. はじめに

この講義では、例外処理で重要なテクニック ― 例外の連鎖(exception chaining)を解説します。このテクニックにより、ある例外を別の例外で「ラップ」した場合でも、エラーの根本原因に関する情報を失わずに済みます。

実際のアプリケーションでは、エラーが呼び出しスタックの深い場所で発生することがよくあります。たとえばデータベース、ファイルシステム、ネットワークの操作中です。データベースにアクセスするメソッドがあり、SQLException を投げる可能性があるとします。しかしビジネスロジック層では技術的な詳細でコードを「散らかしたく」ないため、たとえば UserManagementException のような独自の例外を投げたい場合があります。

新しい例外を単に投げたらどうなるでしょうか?

try {
    // データベース関連の処理
} catch (SQLException e) {
    throw new UserManagementException("ユーザー処理中にエラーが発生しました");
}

問題点:
この場合、データベースで実際に何が起きたのか(そしてスタックトレース!)に関する情報が失われます。ログには UserManagementException だけが表示され、原因は不明のままです。

2. 解決策: 元の例外をラップする(chaining)

Java では、新しい例外のコンストラクタに元の例外を原因(cause)として渡し、ある例外を別の例外で「ラップ」できます。これを 例外の連鎖 と呼びます。

やり方

多くの標準/自作の例外には、第2引数として Throwable 型の cause を受け取るコンストラクタがあります:

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

使用例:

try {
    // データベース関連の処理
} catch (SQLException e) {
    throw new UserManagementException("ユーザー処理中にエラーが発生しました", e);
}

この状態でスタックトレース(printStackTrace())を確認すると、自分の例外に加え、最初の原因に至るまでの完全な連鎖が見られます。

3. 例外の原因を取得する方法

任意の Throwable オブジェクトは getCause() メソッドを持ち、元の例外を返します(存在しない場合は null)。

例:

try {
    // ...
} catch (UserManagementException e) {
    Throwable cause = e.getCause();
    if (cause != null) {
        System.out.println("根本原因: " + cause);
    }
    e.printStackTrace();
}

なぜ必要か?

  • デバッグのため: 上位レベルで「何がうまくいかなかったか」だけでなく、スタックの深い箇所でエラーがどこで起きたかも分かります。
  • ロギングのため: エラー連鎖全体をログに記録できます。
  • アプリケーションの層間で情報を渡すため: ビジネス層が技術的な例外を自分の例外で「ラップ」しても、詳細を失いません。

4. 例: 実アプリでの例外の連鎖

たとえば、データベースからユーザーを読み込むメソッドがあるとします:

public User loadUser(String username) throws UserManagementException {
    try {
        // SQLException を投げる可能性のあるコード
        // ...
    } catch (SQLException e) {
        throw new UserManagementException("ユーザーを読み込めませんでした: " + username, e);
    }
}

ここで UserManagementException は自作の例外です:

public class UserManagementException extends Exception {
    public UserManagementException(String message, Throwable cause) {
        super(message, cause);
    }
}

エラーが発生するとどうなるか?

  • ログには自作の例外と元の SQLException の両方が、詳細つきで表示されます。
  • 必要に応じて getCause() で根本原因にアクセスできます。

5. 例外の連鎖時のスタックトレースの見え方

出力例:

UserManagementException: ユーザーを読み込めませんでした: vasya
    at UserService.loadUser(UserService.java:15)
    ...
Caused by: java.sql.SQLException: Connection refused
    at ...

ここですべてが分かります: 呼び出しの完全な連鎖、どこでビジネスエラーが発生したか、原因となった技術的な例外は何か、が示されます。

6. 実践: 例外の連鎖を実装する

ステップ1: 自作の例外を作る:

public class UserManagementException extends Exception {
    public UserManagementException(String message) {
        super(message);
    }
    public UserManagementException(String message, Throwable cause) {
        super(message, cause);
    }
}

ステップ2: 連鎖を使う:

try {
    // 危険な処理
} catch (SQLException e) {
    throw new UserManagementException("DB 操作中にエラーが発生しました", e);
}

ステップ3: 最上位での処理:
プログラムの最上位で自作の例外を捕捉し、エラーメッセージと原因のチェーンを一緒に出力します。

public class Main {
    public static void main(String[] args) {
        try {
            runUserManagement();
        } catch (UserManagementException e) {
            System.err.println("エラーが発生しました: " + e.getMessage());
            // 原因のチェーンを出力
            Throwable cause = e.getCause();
            while (cause != null) {
                System.err.println("原因: " + cause.getMessage());
                cause = cause.getCause();
            }
        }
    }

    private static void runUserManagement() throws UserManagementException {
        try {
            // DB エラーの擬似発生
            throw new SQLException("DB への接続がありません");
        } catch (SQLException e) {
            throw new UserManagementException("DB 操作中にエラーが発生しました", e);
        }
    }
}

7. 例外の連鎖でありがちなミス

ミス1: cause を付けずに新しい例外を投げる。

catch (SQLException e) {
    throw new UserManagementException("エラー", /* cause がない! */);
}

よくない点: 根本原因に関する情報が失われます。

ミス2: 自作例外に cause 付きコンストラクタを用意していない。
自作の例外クラスに Throwablecause を受け取るコンストラクタがない場合、原因を渡せません ― 手動で追加する必要があります。

ミス3: 例外を捕捉して握りつぶし、先へ伝播しない。

catch (SQLException e) {
    // ログに書くだけで黙る
}

よくない点: エラーが「消え」、プログラムは不正な状態のまま動き続けます。

try {
    userService.loadUser("vasya");
} catch (UserManagementException e) {
    System.err.println("エラー: " + e.getMessage());
    if (e.getCause() != null) {
        System.err.println("根本原因: " + e.getCause());
    }
    e.printStackTrace();
}
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION