Claude CodeでRustのpanicを読み、backtraceから原因の関数を辿る
RustのpanicメッセージとRUST_BACKTRACE=fullの出力を、自分のクレートのフレームだけに絞ってClaude Codeに渡す方法。unwrap由来のpanicは再現テストを先に書いてResult化します。
Rustのpanicは、メッセージ全文とbacktraceをそのまま貼るより、自分のクレートのフレームだけを残して渡したほうがClaude Codeは原因に早く着きます。unwrapやexpectが原因のpanicなら、修正の前に再現テストを書かせ、そのうえでResultを返す形に直す流れが安定します。
この記事では、panicメッセージの読み方、RUST_BACKTRACE=full の取り方、フレームの絞り込み、依頼文の書き方、再現テストを先に置く修正の順に進めます。
panicメッセージで最初に見る3か所
panicの出力は3つの情報でできています。Rust本のpanic解説に出てくる例で言うと、次の形です。
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace1行目の src/main.rs:4:6 は、panicが起きたファイル・行・桁です。2行目が原因の説明で、最後の note: がbacktraceの取り方を案内しています。
注意したいのは、1行目の場所が自分のコードとは限らない点です。panic!を呼んでいるのが依存先のコードなら、報告される場所も依存先になります。Rust本はこの点を明記しており、自分のコードのどこから呼ばれたかはbacktraceで辿るとしています。
つまりClaudeに「src/main.rs:4:6 を直して」と頼む前に、その行が自分のクレートかどうかを見る必要があります。
RUST_BACKTRACE=1とfullの違い
RUST_BACKTRACE に0以外の値を入れるとbacktraceが出ます。1 の出力は一部の詳細が省かれ、末尾に RUST_BACKTRACE=full で詳細表示になる旨のnoteが付きます。
RUST_BACKTRACE=1 cargo run
RUST_BACKTRACE=full cargo run行番号まで出すにはデバッグ情報が要ります。Rust本によれば、--release を付けない cargo build や cargo run では既定で有効です。std::backtraceの解説も、ファイル名と行番号の報告にはデバッグ情報付きのコンパイルが必要だと書いています。
もう1つ、同じ解説にある性質を押さえておきます。環境変数の状態は最初のbacktraceが作られた時点でキャッシュされるため、実行中に値を変えても反映されないことがあります。テストやプログラムの外側で、起動前に設定してください。
どちらをClaudeに渡すか
1 で原因の関数が見えるなら十分です。ジェネリクスやクロージャが挟まって呼び出しの経路が読めないときだけ full を使います。full は長くなるので、次の節の長さの問題が効いてきます。
backtraceの精度は、std::backtraceの解説の言葉で言えばベストエフォートです。行番号や関数名がずれることもあるため、Claudeが指した行は必ずファイルを開いて確かめさせます。
長いbacktraceはファイルに逃がす
Claude Codeは、Bashで実行したコマンドの出力を結果として読み戻します。tools-referenceの「Output limits」節によれば、通常の結果は約30,000文字までインラインで返ります。コマンドが失敗扱いになると、戻るのは約10,000文字の先頭と末尾の抜粋で、ファイルのパスは付きません。
panicで落ちるコマンドは失敗扱いになりやすく、full の出力が長いと途中が抜けます。自分のクレートのフレームは、まさにその途中にあることが多い箇所です。
対策は、出力をファイルに保存してからClaudeに読ませることです。
RUST_BACKTRACE=full cargo test parse_amount \
> panic.log 2>&1あとは依頼文で @panic.log と指定します。best-practicesには、ファイルは @ とパスで内容を含められるとあります。
自分のクレートのフレームだけを残す
backtraceのフレームは、先頭から「panicを起こした標準ライブラリの内部」「自分のコード」「呼び出し元のランタイム」の順に並ぶのが普通です。Rust本は、読み方を「上から読み、自分が書いたファイルが出てくるところが問題の起点」と説明しています。その上の行は自分のコードが呼んだ先で、下の行は自分のコードを呼んだ側です。
この性質を使えば、クレート名で行を絞れます。次の例は、クレート名が invoice_tool の場合です。
grep -E -A1 '^ +[0-9]+: <?invoice_tool::' panic.log \
| grep -v '^--' > own-frames.txt-A1 が効くのは、各フレームの次の行に at ./src/amount.rs:3:31 のような場所が出るためです。次のような形に縮みます(形式はRust本のlistingに沿った例です)。
3: invoice_tool::amount::parse_amount
at ./src/amount.rs:3:31
4: invoice_tool::report::total
at ./src/report.rs:12:20
5: invoice_tool::main
at ./src/main.rs:8:5番号3が最も内側の自分のコードで、そこからmainまで呼び出しの経路が3段で読めます。
絞り込みで落ちるもの
絞ると、panicの直接の原因になった標準ライブラリの関数(Result::unwrap など)が消えます。そこで、渡すものは3点に分けます。
- panicの1〜2行目(メッセージと場所)
- 自分のクレートのフレーム
- 自分のコードの直前にある1フレーム
3点目が分かると、unwrap なのか、添字アクセスなのか、expect なのかが一目で決まります。クロージャやasync境界で自分のフレームが途切れているときは、絞らない全文のファイルを @panic.log で渡し直してください。
依頼文はこの順番で書く
best-practicesは、症状と「直った状態」を先に言う具体的な依頼が手戻りを減らすとしています。同じ考え方で、panicの依頼は次のように書けます。
cargo test parse_amount が panic します。
panic のメッセージと自分のクレートのフレームは
@own-frames.txt、全文は @panic.log です。
まず原因の関数と、その入力値を特定してください。
修正はまだしないでください。特定の根拠として、
該当行をファイルで開いて確認してください。最後の「修正はまだしない」が効きます。原因の特定と修正を分けると、Claudeの仮説を先に読めます。仮説が外れていれば、コードに手が入る前に止められます。
panicから修正までの流れ
- 1
出力をファイルに保存する
RUST_BACKTRACE=fullでテストを回し、panic.logに落とします。 - 2
自分のクレートのフレームを抜く
own-frames.txtを作り、全文と一緒に渡します。 - 3
原因だけを特定させる
関数・入力値・該当行を答えさせ、修正は止めます。
- 4
再現テストを先に書かせる
赤になることを
cargo testの出力で確かめてから、直しに入ります。
unwrapとexpectのpanicはResultに開く
panicの原因が Result::unwrap() や expect なら、メッセージで見分けられます。Rust本の例では、存在しないファイルを開いたときのunwrapは次のように出ます。
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }expect なら、渡したメッセージが先頭に出て、続けてエラーの中身が付きます。Rust本は、実運用のコードでは多くの人がunwrapよりexpectを選び、「なぜここは常に成功すると考えたか」を書くと説明しています。
ここで分かれ道があります。
- 入力が外部由来で、失敗は起こりうる: Resultを呼び出し元に返す
- 失敗は設計上ありえない:
expectに理由を書いて残す
Resultを返す書き方は、Rust本のエラー伝播の節にあるとおり ? 演算子で短く書けます。Claudeには、どちらに倒すかを自分で決めさせず、入力がどこから来るかを答えさせてから選びます。
再現テストを先に置く
best-practicesの例に「再現するfailing testを書いてから直す」があります。Rustでは次のように使えます。例として、空文字列を渡すとunwrapで落ちる関数を考えます。
pub fn parse_amount(s: &str) -> u64 {
s.trim().parse().unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_amount_does_not_panic() {
let _ = parse_amount("");
}
}このテストは、今の実装ではpanicで落ちます。ここまでを cargo test で実行させ、赤い出力を読むのが最初の確認です。テストが赤にならないなら、再現できていないので、修正に進みません。
赤を確認したら、関数をResultを返す形に変えます。
#[derive(Debug)]
pub enum AmountError {
Empty,
Invalid(std::num::ParseIntError),
}
pub fn parse_amount(s: &str) -> Result<u64, AmountError> {
let s = s.trim();
if s.is_empty() {
return Err(AmountError::Empty);
}
s.parse().map_err(AmountError::Invalid)
}戻り値の型が変わるので、テストも同時に直す必要があります。
#[test]
fn empty_amount_is_an_error() {
assert!(matches!(parse_amount(""), Err(AmountError::Empty)));
}署名が変わると、呼び出し側の invoice_tool::report::total も更新が要ります。修正後に cargo test と cargo check の両方を実行させ、出力そのものを見せるよう頼んでください。best-practicesも「全部通りました」だけでなく、実行したコマンドと出力を求めるよう勧めています。
CLAUDE.mdに書いておく3行
毎回依頼文に書かなくて済むよう、リポジトリの CLAUDE.md に手順を置けます。
## Rust の panic 調査
- backtrace は `RUST_BACKTRACE=full` でファイルに保存してから読む
- クレート名は `invoice_tool`。このクレートのフレームを起点にする
- panic の修正は、再現テストが赤になることを確認してから行うCLAUDE.mdが長くなりすぎる場合の扱いはCLAUDE.mdが長すぎる警告の直し方にあります。テストの出力そのものを短くしたいなら、nextestで回すRust CLIの手順が使えます。Webサーバー側のpanicなら、AxumのRust API開発の cargo check とclippyの運用と組み合わせられます。
つまずきやすい点
落とし穴
releaseビルドだと行番号が出ない
cargo build --release のバイナリは、既定のままでは行番号が付かない場合があります。Rust本が既定で有効とするのは --release なしのビルドです。本番ビルドのpanicを調べるなら、同じ入力をデバッグビルドで再現できないか先に試します。
RUST_BACKTRACEはプログラムを起動する前に設定します。実行中に変えても反映されないことがあります- 絞った
own-frames.txtだけで原因が決まらないときは、全文のpanic.logを渡し直します - Claudeが示した行番号は、backtraceの精度がベストエフォートである以上、ファイルを開いて照合させます
- 修正案が
unwrap_or_default()で握りつぶす形なら、再現テストが何を保証しているかを確認します。値が変わっただけで、失敗は消えていないことがあります
まとめ
panic調査の要は、Claudeに渡す情報の質です。メッセージ、自分のクレートのフレーム、全文のファイルを分けて渡し、原因の特定と修正を2回に分けると、途中の仮説を読めます。unwrap由来のpanicは、赤になる再現テストを先に置くことで、直したあとに何が保証されたのかも見えるようになります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。