JavaのスタックトレースとCaused byの読み方を解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアが例外の根本原因を追うイラスト。

開発現場メモ

JavaのスタックトレースでCaused byはどこまで追う?根本原因の見つけ方

AIによる要約

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

新人SE
新人SE
ログが何百行もあり、Caused byも3つあります。一番上と一番下のどちらを見ればよいですか?
ポンコツSE
ポンコツSE
上は処理全体で見えた例外、下は原因の連鎖です。下へ追いつつ、該当ブロックの自作コードへ戻るのが基本です。

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:52OrderRepositoryAdapter.java:74と、customerIdを作った処理を確認します。

読む順番は5段階に固定する

  1. 発生時刻、リクエストID、処理名を確認する
  2. 一番上の例外型とメッセージを読む
  3. Caused byを下へ追い、具体的な原因メッセージを探す
  4. 原因ブロック内で最初の自作コード行を探す
  5. その行へ渡った入力値と直前処理を確認する

検索エンジンへ例外全文を入れる前に、この5点を確認します。同じNullPointerExceptionでも、発生行と入力条件が違えば原因は異なります。

例外名は現象、行番号は場所、入力条件は再現方法です。3つを揃えると修正へ進めます。

Caused byはどこまで追うか

基本は、Caused byがなくなるまで下へ追います。Javaの例外は別の例外をcauseとして保持でき、上位層が意味を付けて包み直すことがあるためです。

位置分かること
最上位利用者や処理全体から見えた失敗
中間のCaused bySpring・JPAなど各層での変換結果
深いCaused byDB・ファイル・通信など具体的な失敗
自作コード行値を作った・呼び出した修正候補

ただし「最下段の例外を直せばよい」とは限りません。たとえば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記録していないか

読みやすい例外処理とコードを学ぶ参考書

スタックトレースを読みやすくするには、例外名、メソッド名、責務の分かれたコードも重要です。調査しやすいコードの基本を併せて学べます。

書籍「リーダブルコード」の表紙
書影:O'Reilly Japanより引用
PR 新人・若手エンジニア向け

リーダブルコード

著 / 角 征典 訳

コードレビューの「読みづらい」を減らす、最初の一冊。

命名、コメント、条件分岐、変数、メソッド分割を短い実例から学べます。Java専用書ではありませんが、現場で使える読みやすさの判断基準が身につきます。

  • レビュー指摘の理由と直し方を整理したい
  • 動くだけでなく、読みやすいコードを書きたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

スタックトレースは、先頭の例外で処理全体を把握し、Caused byを最後まで追って具体的な原因を探します。その原因ブロック内で最初に現れる自作コードの行番号を開き、入力条件と直前処理を確認します。

最下段の例外名だけを結論にせず、修正可能な自作コードへ戻ることが重要です。例外を包み直すときはcauseを保持し、調査結果を現象・原因例外・行番号・再現条件の4点で共有しましょう。

-開発現場メモ
-, , ,