Caused byの次、どこを開く? 例外名から、自分のコードへ。

実装・エラー解決

Javaのスタックトレースの読み方|Caused byの後、どのコードを開けばいい?

ログに何十行も赤い文字が出ている。Caused byが大事だとは聞いたけれど、一番下の例外名を検索した後、どのコードを直すのか分からない。

この記事は、Javaの不具合調査を初めて担当する人向けです。ゴールは例外名を覚えることではなく、失敗した値と、その値を作った処理を調べ始められることです。

原因例外を読んだら、自分たちのコードへ戻る。
一番下の例外名は「何が失敗したか」の手がかりです。それだけで、悪い入力を作った場所や直し方までは決まりません。

まず、調べる一件を選ぶ

同じ時刻に複数のリクエストが動く環境では、近くに出たエラーが同じ失敗とは限りません。操作時刻、処理名、リクエストIDなどで、対象の一件をそろえます。

次のログは説明用に短くした例です。注文CSVの金額を数値へ変換する処理で失敗しています。

java.lang.IllegalStateException: 注文CSVの取込に失敗しました
    at ImportService.importRow(ImportService.java:18)
Caused by: java.lang.NumberFormatException: For input string: "1,200"
    at java.base/java.lang.Integer.parseInt(Integer.java:...)
    at ImportService.parseAmount(ImportService.java:24)
    at ImportService.importRow(ImportService.java:16)
    ... 1 more

Integer.parseIntの内部行を直す話ではありません。まずImportService.parseAmountが何を渡したかを見ます。この例なら、桁区切りのカンマを含む文字列が数値変換へ渡っています。

上から文脈、下から手がかり、自作コードで合流する

読む場所この例から分かること次に見る場所
先頭の例外注文CSV取込という処理が失敗した対象の取込操作と入力
Caused by数値変換で失敗した変換へ渡した文字列
原因に近い自作コードparseAmountから変換を呼んだ金額を取り出した処理と入力仕様

Caused byは、例外に保持された原因を示します。外側の例外と原因例外は別の情報です。JavaのThrowable公式仕様でも、この連鎖とスタック出力が定義されています。

自作コードの行を開いたら、ログを出した版と手元のコードの版が一致するかも確認します。別バージョンの18行目を見ても、違う処理を調べてしまいます。

カンマを消せば直る、と決める前に入力の約束を確認する

このログから言えるのは、「1,200という文字列を、今回の変換処理が受け付けなかった」までです。どう直すかは入力仕様で変わります。

入力の約束直す方向
桁区切りを許す許す書式を確定し、その書式を扱う変換を用意する
桁区切りを許さない入力の問題として、該当行と項目を伝えられるようにする
本来は数量欄を読んでいる列の対応やCSV解析の誤りを直す

「例外を消す」だけなら文字を除去して通せるかもしれません。しかし列の対応がずれているのに値を加工すると、間違った金額を登録する可能性があります。値・取り出した場所・入力仕様の三つを照合してから変更します。

... moreは別のエラー件数ではない

... 1 moreのような表記は、外側の例外と共通する末尾の呼び出し経路を省略したものです。「あと一つエラーがある」という意味ではありません。必要なら外側のスタックとつなげて読みます。

Suppressedがある場合は、causeとは別の補足例外です。例えばtry-with-resourcesでは、本体の処理に加えてcloseも失敗した場合、本体の例外にcloseの失敗が抑制例外として残ります。原因チェーンの続きとして機械的に扱わず、後片付けの失敗も含む別の情報として読みます。Oracleの解説

相談するときは、例外名だけを送らない

次の四行があれば、相談相手は調査を続けやすくなります。

現象:検証環境で注文CSVの取込が失敗した。
確認済み:金額変換へ "1,200" が渡り、NumberFormatExceptionになった。
未確認:桁区切りを許す仕様か、列の対応が正しいか。
次の確認:CSVの項目定義と、parseAmountの呼び出し元を照合する。

「原因はNumberFormatExceptionです」だけでは、まだコードを変える判断はできません。「何が渡った」「何はまだ分からない」を足します。共有用ログには認証情報や実データを貼らず、必要な場合は権限管理された原本の場所を案内します。

例外を包む側になったら、causeを残す

呼び出し元へ処理の文脈を付けたいときは、元の例外を第二引数に渡します。

try {
    return Integer.parseInt(amount);
} catch (NumberFormatException e) {
    throw new IllegalStateException("注文金額の変換に失敗しました", e);
}

メッセージだけを新しい例外へコピーすると、元の型や発生箇所を追いにくくなります。この例はJava標準APIだけで動く断片です。業務例外へ変換する場合も、その例外クラスがcauseを受け取れる形にします。

次に長いログを見たら、先頭の失敗、原因例外、自作コードの行をそれぞれ一つ選んでください。その行に渡った値と、期待される入力を比べるところから調査を始められます。

-実装・エラー解決
-, ,