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 付きコンストラクタを用意していない。
自作の例外クラスに Throwable の cause を受け取るコンストラクタがない場合、原因を渡せません ― 手動で追加する必要があります。
ミス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();
}
GO TO FULL VERSION