ログに何十行も赤い文字が出ている。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 moreInteger.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を受け取れる形にします。
次に長いログを見たら、先頭の失敗、原因例外、自作コードの行をそれぞれ一つ選んでください。その行に渡った値と、期待される入力を比べるところから調査を始められます。
