AIによる要約
e.getMessage()が返すのは例外メッセージのStringだけです。SLF4JへThrowableを渡していないため、発生クラスや行番号を含むスタックトレースは記録されません。log.error("保存失敗", e)またはlog.error("userId={}の保存に失敗", userId, e)のように例外自体を最後の引数へ渡します。ただし、同じ例外を各層で繰り返しERROR記録しないことも重要です。
この記事はXへ投稿したJavaクイズの詳しい解説です。まず問題を解きたい方は、Xの元ポストを開くことができます。


障害調査で最も困るのは、エラーが起きた事実は分かるのに、どの処理経路で起きたか追えないログです。e.getMessage()だけを出すコードは一見丁寧ですが、スタックトレースを捨てています。
一方、例外をすべてのController、Service、RepositoryでERROR出力すると、同じ障害が何行も記録されます。現場で必要なのは『とにかくログを増やす』ことではなく、例外を扱う境界で、調査に必要な識別子とThrowableを一度残すことです。
この記事のポイント
- e.getMessage()はStringでありスタックトレースを含まない
- ThrowableをSLF4Jの最後の引数へ渡す
- プレースホルダーと例外を同時に使える
- 業務識別子は残すが個人情報を出しすぎない
- 同じ例外の二重・三重ログを避ける
まずはXのJavaクイズを確認する

try {
userService.save(user);
} catch (Exception e) {
log.error("Save failed: {}", e.getMessage());
}答えはメッセージだけです。e.getMessage()の戻り値はStringなので、SLF4Jは例外として扱えません。

e.getMessage()で失われる情報
例外メッセージには『Connection refused』や『User not found』などの短い説明が入ります。しかし、例外型、発生したクラスと行番号、呼び出し元の経路、Caused byでつながる根本原因は含まれません。
Save failed: Connection refusedこの1行だけでは、保存処理、監査ログ、外部API、DBのどこで失敗したか判断できません。同じメッセージを出す箇所が複数あれば検索でも特定できず、再現待ちになります。
Throwableを最後の引数へ渡す
SLF4Jでは、メッセージの後ろへThrowableを渡すとスタックトレースが出ます。例外メッセージを自分で連結する必要はありません。
try {
userService.save(user);
} catch (RuntimeException e) {
log.error("利用者の保存に失敗", e);
throw e;
}ただし、このServiceで記録した後に上位でも同じ例外をERROR記録すると重複します。ここがシステムの例外記録境界か、上位の共通例外ハンドラへ任せるかを決めてください。
プレースホルダーと例外を一緒に使う
調査では、どの注文、利用者、連携ファイルが失敗したかという識別子も必要です。プレースホルダーへ業務IDを渡し、最後にThrowableを置きます。
log.error("userId={} の保存に失敗", userId, e);最後の引数がThrowableで、対応する{}がない場合、SLF4Jは例外として扱います。引数の順序を崩すと、例外が文字列表現として埋め込まれ、期待するスタックトレースにならないことがあります。実際のログ出力も確認します。
ログへ残す情報と残さない情報
業務ID、処理名、外部連携先、ファイル管理番号など、検索に使える情報を残します。一方、氏名、メールアドレス、アクセストークン、パスワード、カード番号、入力本文全体などをそのまま出してはいけません。
例外メッセージにも機密情報が含まれる
- HTTPクライアントの例外がリクエストヘッダーを含まないか
- SQL例外が入力値を含まないか
- ファイルパスに利用者名が含まれないか
- 本番ログの閲覧権限と保存期間が適切か
『Throwableを出せば安全』ではありません。例外ライブラリのメッセージ内容を確認し、マスキングや安全なコンテキストだけを残す共通方針を使います。
ERRORを出す場所を決める
同じ例外を下位で記録して再throwし、上位でも記録すると、監視が複数件の障害として数える可能性があります。Repositoryは例外を変換して上位へ渡し、共通例外ハンドラが1回記録する、といった境界を決めます。
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(RuntimeException.class)
ResponseEntity<ErrorResponse> handle(RuntimeException e) {
log.error("予期しないエラー", e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("SYSTEM_ERROR"));
}
}下位層では、そこで復旧する場合や、上位へ渡す前にしか取得できない情報がある場合に記録を検討します。ログの有無だけでなく、監視通知と例外レスポンスの責務も合わせます。
printStackTraceを本番コードで使わない
printStackTrace()は標準エラーへ直接出力します。ログレベル、時刻、スレッド、出力先、ローテーションなど、アプリケーションのログ設定を通らないため、本番運用では追跡しにくくなります。
catch (IOException e) {
e.printStackTrace(); // 本番コードでは避ける
}開発中に一時的に確認したコードが残ることもあります。レビューや静的解析で検出し、SLF4Jへ統一します。標準出力のSystem.out.printlnで例外を出す書き方も同じ理由で避けます。
テストと動作確認で見るポイント
ログ出力そのものを単体テストで細かく固定しすぎると、文言変更で壊れやすくなります。ただし、監査や障害通知の契約になっているログは、テスト用Appenderなどを使ってレベル、識別子、Throwableの有無を確認できます。
確認するログの例
- level: ERROR
- message: userId=U001 の保存に失敗
- throwable: DataAccessException
- cause: SQLException最低限、開発環境で意図的に例外を発生させ、スタックトレースとCaused byが実際のログファイルへ出ることを確認します。ロガー実装や設定によって出力形式が変わるため、コードだけで完了としません。
現場レビューでよくある指摘
ログのレビューでは、メッセージの日本語よりも、調査に必要な経路と識別子が残り、重複通知や情報漏えいを起こさないかを見ます。
// レビューコメント例
e.getMessage()だけでは発生行とCaused byが残りません。
例外eを最後の引数へ渡してください。
// レビューコメント例
この例外は共通ハンドラでもERROR記録されます。
同じ障害が二重通知されるため、どちらを記録境界にするか決めてください。
// レビューコメント例
リクエスト本文全体には個人情報が含まれます。
調査に必要なrequestIdとuserIdだけを残してください。ログは正常系のテストだけでは評価できません。例外を発生させ、ログ収集基盤で検索できるか、アラートが何件になるか、利用者向けレスポンスへ内部情報が漏れないかまで確認します。
提出前のセルフチェック
レビュー前に確認すること
- Throwableを最後の引数へ渡したか
- e.getMessageだけで終わっていないか
- 検索できる業務IDやrequestIdがあるか
- 個人情報や秘密情報を出していないか
- 同じ例外を複数層でERROR記録していないか
- printStackTraceやSystem.outが残っていないか
- Caused byまでログへ出ることを確認したか
- 監視通知の単位と一致しているか
例外とログを現場設計として考える参考書
ログは文法だけでなく、例外の伝播、責務分担、障害調査の設計とつながっています。現場の設計原則と合わせて読むと、どの層で何を残すか判断しやすくなります。
現場で役立つシステム設計の原則
変更しやすい業務システムの設計原則を、現場目線で学ぶ。
Controller、Service、Entityの責務を考える土台になります。業務ロジックの置き場所に迷う人向けです。
- クラスの責務を整理したい
- 変更しやすい設計を学びたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
e.getMessage()は例外メッセージのStringだけを返すため、スタックトレースは出ません。SLF4Jへ例外オブジェクトを最後の引数として渡し、発生行とCaused byを残します。
業務識別子は必要ですが、個人情報や秘密情報は出しません。同じ例外を各層で繰り返しERROR記録せず、システムの例外記録境界を決めることが、調査しやすいログにつながります。
