概要
業務システムでは、DB 層で発生した SQLException をサービス層で ServiceException にラップし、さらにコントローラー層で適切なエラーレスポンスに変換する、というレイヤードな例外設計が一般的です。このとき、原因例外(cause)を保持しないままラップすると、障害調査で根本原因にたどり着けなくなります。Java の例外チェーン機構は、Throwable のコンストラクタに cause を渡すことで、例外の因果関係をスタックトレースに残す仕組みです。この記事では、カスタム例外の定義方法、cause の正しい渡し方、getCause() でチェーンを辿って原因を特定するコード、そして例外をログに記録する際の注意点を整理します。e.printStackTrace() を使ってはいけない理由や、例外を握りつぶすアンチパターンについても触れます。
使いどころ
DB アクセス層で発生した SQLException を DataAccessException にラップし、サービス層に業務レベルのエラーとして伝播する
障害発生時にログから例外チェーンを辿り、ServiceException → DataAccessException → SQLException という因果関係を追跡する
外部 API 呼び出しで IOException が発生した際、リトライ対象かどうかを cause の型で判定する
コード例
import java.util.*;
import java.util.logging.*;
public class ExceptionChainExample {
private static final Logger logger =
Logger.getLogger(ExceptionChainExample.class.getName());
record ExceptionInfo(String type, String message) {}
/** カスタム例外: データアクセス層 */
static class DataAccessException extends Exception {
public DataAccessException(String message, Throwable cause) {
super(message, cause);
}
}
/** カスタム例外: サービス層 */
static class ServiceException extends Exception {
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
/** DB アクセス層: 低レベル例外を DataAccessException にラップ */
public static String findUser(int id) throws DataAccessException {
try {
if (id <= 0) {
throw new IllegalArgumentException(
"ID は1以上を指定: " + id);
}
if (id > 100) {
throw new RuntimeException("DB 接続タイムアウト");
}
return "User-" + id;
} catch (Exception e) {
throw new DataAccessException(
"ユーザー取得失敗: id=" + id, e);
}
}
/** サービス層: DataAccessException を ServiceException にラップ */
public static String getUserDisplayName(int id)
throws ServiceException {
try {
return findUser(id).toUpperCase();
} catch (DataAccessException e) {
throw new ServiceException(
"ユーザー表示名の取得に失敗", e);
}
}
/** 例外チェーンを辿って情報リストを構築 */
public static List<ExceptionInfo> buildChain(Throwable e) {
var chain = new ArrayList<ExceptionInfo>();
var current = e;
while (current != null) {
chain.add(new ExceptionInfo(
current.getClass().getSimpleName(),
current.getMessage()));
current = current.getCause();
}
return chain;
}
/** 例外チェーンをインデント付きで表示 */
public static void printChain(Throwable e) {
var chain = buildChain(e);
for (int i = 0; i < chain.size(); i++) {
var info = chain.get(i);
System.out.println(" " + " ".repeat(i)
+ info.type() + ": " + info.message());
}
}
public static void main(String[] args) {
// 正常系
try {
System.out.println(getUserDisplayName(1));
} catch (ServiceException e) {
logger.log(Level.SEVERE, "サービスエラー", e);
}
// DB タイムアウト → 例外チェーン表示
try {
getUserDisplayName(200);
} catch (ServiceException e) {
System.out.println("例外チェーン:");
printChain(e);
logger.log(Level.SEVERE, "ユーザー取得エラー", e);
}
// 引数エラー → 例外チェーン表示
try {
getUserDisplayName(-1);
} catch (ServiceException e) {
System.out.println("例外チェーン:");
printChain(e);
}
}
}Version Coverage
record で ExceptionInfo(型名・メッセージ)を定義し、チェーンの情報をリストとして構造化できる。var で変数宣言が簡潔になる。
// Java 17: record で例外情報を構造化
record ExceptionInfo(String type, String message) {}
var chain = new ArrayList<ExceptionInfo>();
var current = exception;
while (current != null) {
chain.add(new ExceptionInfo(
current.getClass().getSimpleName(),
current.getMessage()));
current = current.getCause();
}Library Comparison
注意点
例外をラップするときは必ず cause を渡すこと。new ServiceException("エラー") のように cause なしで投げると、原因例外のスタックトレースが完全に失われる
catch ブロックで例外を握りつぶす(catch して何もしない)と、障害の兆候が見えなくなる。最低限 logger.log(Level.WARNING, ..., e) で記録すること
e.printStackTrace() はログフレームワークを経由しないため、出力先やフォーマットの制御ができない。運用環境では必ず Logger 経由で記録する
getCause() のチェーンが循環することは通常ないが、不正な実装で自分自身を cause にセットすると無限ループになる。ライブラリ内部の例外を再ラップする場合は cause の中身を確認すること
例外クラスを細分化しすぎると catch ブロックが増えて可読性が下がる。レイヤーごとに1〜2種類の例外に集約するのが実務的なバランス
FAQ
ラップ側のメッセージは業務的な文脈(何をしようとして失敗したか)を書き、元の例外は cause として渡します。cause のメッセージはスタックトレースに自動的に含まれるため、重複させる必要はありません。
while (current.getCause() != null) { current = current.getCause(); } のように、getCause() が null を返すまで辿ります。最後に残った例外が根本原因(root cause)です。
呼び出し側に回復処理を期待する場合は checked(Exception 継承)、プログラムバグや回復不能なエラーは unchecked(RuntimeException 継承)が一般的な基準です。業務システムでは checked を使うケースが多いです。