AIによる要約
スタックトレースは先頭の例外だけで判断せず、Caused byでつながる原因例外を下へ追います。ただし最下段の例外名だけを答えにせず、その原因ブロック内で最初に現れる自作コードのファイル名・行番号を確認します。... 20 moreは省略された共通フレーム、Suppressedは主例外とは別に抑制された例外です。調査結果は「現象・原因例外・自作コードの行・入力条件」をセットで共有します。


Javaの障害調査でスタックトレースを開くと、Spring、Hibernate、Tomcatなどの行が大量に並びます。新人は最初の1行だけを検索したり、最下段の例外名だけを報告したりしがちです。
スタックトレースは、例外がどこで作られ、どのメソッドを通って表面化したかを示す記録です。読む順番を固定すれば、フレームワークの行をすべて理解しなくても、自分たちが直す場所へ到達できます。
この記事のポイント
- 先頭行で処理全体の例外とメッセージを確認する
- Caused byを下へ追い、原因チェーンを確認する
- 原因ブロック内の最初の自作コード行を探す
- 最下段が必ず修正箇所とは限らない
- 例外を包み直すときはcauseを失わない
まずスタックトレースの構造を知る
com.example.order.OrderImportException: 注文取込に失敗しました
at com.example.order.OrderImportService.importFile(OrderImportService.java:52)
at com.example.batch.OrderJob.execute(OrderJob.java:31)
Caused by: org.springframework.dao.DataIntegrityViolationException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(...)
at com.example.order.OrderRepositoryAdapter.save(OrderRepositoryAdapter.java:74)
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(...)
... 18 more
Caused by: java.sql.SQLIntegrityConstraintViolationException:
Column 'customer_id' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(...)
... 25 more
このログでは、最上位にアプリ独自のOrderImportExceptionがあります。その原因がSpringのDataIntegrityViolationException、さらにHibernateとJDBCの例外へつながっています。
最下段のメッセージから、DBのcustomer_idへnullを入れようとしたことが分かります。ただし、修正候補はJDBCドライバではありません。自作コードのOrderImportService.java:52やOrderRepositoryAdapter.java:74と、customerIdを作った処理を確認します。
読む順番は5段階に固定する
- 発生時刻、リクエストID、処理名を確認する
- 一番上の例外型とメッセージを読む
- Caused byを下へ追い、具体的な原因メッセージを探す
- 原因ブロック内で最初の自作コード行を探す
- その行へ渡った入力値と直前処理を確認する
検索エンジンへ例外全文を入れる前に、この5点を確認します。同じNullPointerExceptionでも、発生行と入力条件が違えば原因は異なります。
例外名は現象、行番号は場所、入力条件は再現方法です。3つを揃えると修正へ進めます。
Caused byはどこまで追うか
基本は、Caused byがなくなるまで下へ追います。Javaの例外は別の例外をcauseとして保持でき、上位層が意味を付けて包み直すことがあるためです。
| 位置 | 分かること |
|---|---|
| 最上位 | 利用者や処理全体から見えた失敗 |
| 中間のCaused by | Spring・JPAなど各層での変換結果 |
| 深いCaused by | DB・ファイル・通信など具体的な失敗 |
| 自作コード行 | 値を作った・呼び出した修正候補 |
ただし「最下段の例外を直せばよい」とは限りません。たとえばSocketTimeoutExceptionが最下段でも、タイムアウト設定を増やす前に、上位の自作コードが同じ外部APIをループで100回呼んでいないか確認する必要があります。
最初の自作コードを探す
スタックフレームは、例外が発生した近い呼び出しから順に並びます。原因ブロックの上側から、自社・自案件のパッケージ名を探します。
java.lang.NullPointerException:
Cannot invoke "Customer.getName()" because "customer" is null
at com.example.order.OrderMapper.toResponse(OrderMapper.java:28)
at com.example.order.OrderService.find(OrderService.java:61)
at com.example.order.OrderController.detail(OrderController.java:42)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(...)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(...)
この場合、まずOrderMapper.java:28を開きます。そのうえでcustomerがnullになる仕様か、Repositoryの取得条件が不足したか、テストデータが壊れているかを確認します。
Controller.java:42も自作コードですが、より発生点に近いMapperから見る方が効率的です。ただし、Mapperへnullを渡してはいけない契約なら、呼び出し元Serviceの責任も確認します。
「... 20 more」は何を省略しているか
... 20 moreはログ欠損ではなく、上位例外と共通する末尾のスタックフレームを省略した表記です。原因例外の独自部分を読んだ後、必要なら上位ブロックの続きとつなげて考えます。
Caused by: java.lang.IllegalArgumentException: customerId is required
at com.example.order.Order.create(Order.java:45)
at com.example.order.OrderService.create(OrderService.java:38)
... 24 more
この例ではOrder.java:45とService.java:38が原因固有のフレームです。その先24行は上位例外と共通する呼び出し経路なので省略されています。
Suppressedは主原因と分けて読む
java.io.IOException: CSVの読み込みに失敗しました
at com.example.importer.CsvImporter.read(CsvImporter.java:40)
Suppressed: java.io.IOException: Streamをcloseできませんでした
at com.example.storage.RemoteStream.close(RemoteStream.java:88)
Caused by: java.net.SocketTimeoutException: Read timed out
at ...
try-with-resourcesでは、本体処理の例外を主例外として残し、close時の例外をSuppressedへ保持することがあります。この場合の主な失敗は読み込みタイムアウトで、close失敗は追加情報です。
Suppressedを無視してよいという意味ではありません。接続解放の失敗が続けば資源枯渇につながるため、主原因を調べた後に影響を確認します。
例外を包み直すときはcauseを残す
try {
csvReader.read(file);
} catch (IOException e) {
// NG: 元の例外とスタックトレースが失われる
throw new OrderImportException("取込に失敗しました");
}
try {
csvReader.read(file);
} catch (IOException e) {
// OK: causeとして元例外を保持する
throw new OrderImportException(
"取込に失敗しました: " + file.getName(),
e);
}
元例外を渡さないと、Caused byにIOExceptionや発生行が残りません。独自例外へ変換するなら、利用者や上位層に必要な文脈をメッセージへ加え、技術的原因はcauseへ保持します。
メッセージへ機密情報を入れない
パスワード、アクセストークン、個人情報、カード番号を例外メッセージへ含めると、ログや監視通知へ残ります。調査に必要な業務IDも、現場のログ方針に従って扱ってください。
log.errorしてからthrowすると重複しやすい
try {
repository.save(order);
} catch (DataAccessException e) {
log.error("注文保存に失敗しました", e);
throw e;
}
上位の共通例外ハンドラでも同じ例外をERROR記録するなら、同じスタックトレースが2回出ます。障害件数を二重に数える原因にもなるため、原則として「処理を継続する層」または「最終的に扱う境界」で1回記録します。
catchする目的が文脈追加ならcauseを保持してthrowし、ログは上位へ任せます。代替処理で回復するならWARNなどを記録し、throwしない選択もあります。
調査メモは4点で共有する
現象: 注文CSV取込の3件目で処理が失敗する 原因例外: SQLIntegrityConstraintViolationException customer_id cannot be null 自作コード: OrderImportService.java:52 customerCodeからcustomerIdへ変換する処理 再現条件: 顧客コードが空のCSV行を取り込む
「NullPointerExceptionでした」だけでは、他メンバーが同じログを読み直す必要があります。時刻やリクエストIDも添え、原因候補と確認済み事実を分けて共有します。
現場レビューでよくある指摘
// レビューコメント例 独自例外へ変換する際に元のIOExceptionを渡していないため、 発生箇所を追えません。causeを保持してください。 // レビューコメント例 catch内でERRORを出して同じ例外を再throwしており、 共通ハンドラと二重記録になります。ログを出す境界を揃えてください。 // 調査コメント例 最上位のRuntimeExceptionではなく、最下段のDB例外はNOT NULL違反です。 OrderMapper.java:28でcustomerIdがnullになる入力条件を確認します。
提出・報告前のセルフチェック
- 発生時刻と対象リクエストを特定したか
- 先頭の例外と全Caused byを確認したか
- 原因ブロックの最初の自作コード行を開いたか
- 最下段の例外名だけで結論を出していないか
- 入力値・DBデータ・直前処理を確認したか
- Suppressedと主例外を分けて読んだか
- 例外変換でcauseを保持したか
- 同じ例外を複数層でERROR記録していないか
読みやすい例外処理とコードを学ぶ参考書
スタックトレースを読みやすくするには、例外名、メソッド名、責務の分かれたコードも重要です。調査しやすいコードの基本を併せて学べます。
リーダブルコード
コードレビューの「読みづらい」を減らす、最初の一冊。
命名、コメント、条件分岐、変数、メソッド分割を短い実例から学べます。Java専用書ではありませんが、現場で使える読みやすさの判断基準が身につきます。
- レビュー指摘の理由と直し方を整理したい
- 動くだけでなく、読みやすいコードを書きたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
スタックトレースは、先頭の例外で処理全体を把握し、Caused byを最後まで追って具体的な原因を探します。その原因ブロック内で最初に現れる自作コードの行番号を開き、入力条件と直前処理を確認します。
最下段の例外名だけを結論にせず、修正可能な自作コードへ戻ることが重要です。例外を包み直すときはcauseを保持し、調査結果を現象・原因例外・行番号・再現条件の4点で共有しましょう。
