コードの書き方を学んでいる人なら、間違いなく恐ろしいスタック トレースに遭遇したことがあるでしょう。そして私と同じように、文字化けした出力を読まないよう訓練したことがあるはずです。しかし、それらはナンセンスではありません。10 分かけて繰り返しパターンを確認すると、重要なスキルのロックを解除できます。バグを解決することはコードを書く上で不可欠な部分であるため、スタック トレースの読み方を学ぶことは避けられません。
コールスタックとは何ですか?
関数の状態と呼び出し履歴を記録する重要なコンポーネント
コール スタック (別名スタック) は、関数の状態を追跡するのに役立つデータ構造です。これは、一連のメモリ アドレス (ポインタ) とスタック フレームで構成されます。
スタック フレームはコンテナのようなものです。実行中の 1 つの関数 (変数など) の状態を保存します。関数呼び出しが発生すると、新しいフレームがスタックの最上位に配置され、関数が返されるとフレームは破棄されます。
呼び出しスタックは、呼び出しシーケンスの順序で配置されたフレームの垂直列のようなものと考えることができます。ポインタを使用して、各フレームがメモリ内のどこに存在するか、およびスタックの最上位を追跡します。
さらに詳しく知りたい場合は、ハーバード大学の有名な CS50 コースのビデオをご覧ください。
彼らの YouTube チャンネルには、興味深いと思われる数十のコースが用意されています。
サイトを磨き上げるための 6 つの JavaScript スニペット
構築しているあらゆるサイトにとって、迅速かつ簡単に成功します。
スタック トレースはどのように編成されますか?
スタック フレームの順序を反映します。
スタック トレースは、失敗した関数に至る呼び出し順序を示します。これは、各関数呼び出しのファイルや行番号などの有用な情報を伝えます。
スタック トレースを理解するには、クラッシュが発生した問題のフレームから始めるのが最適です。スタック フレームは順番に並べられるため、クラッシュしたフレームは最新のものである必要があります。スタック トレースのどちらの端に情報が表示されるかは、使用した特定のランタイムまたはコンパイラによって異なります (先頭または末尾のいずれか)。
いくつかの例を見てみましょう。
次の 3 つのスタック トレースはすべて、3 つの関数呼び出し (「one」、「two」、および「three」) を追跡します。「three」はゼロで除算することでプログラムをクラッシュさせます。
これは JavaScript (Node.js) スタック トレースで、クラッシュしたフレームが上部にあることがわかります。
-
注釈「1」は、問題の原因となっている行の内容を示します。
-
注釈「2」は、問題が発生したファイルを示します。
-
注釈「3」は問題が発生した関数名を示します。
-
注釈「4」は問題のある行番号と文字番号をそれぞれ定義します。
スタック トレースに追加の項目があることに注意してください。下部でグレー表示されているものは、Node.js が行う追加の呼び出しを表します。 「Object」以降のすべてがコードの始まりです。
以下のコードから、4 行目の文字 15 が除算記号であることがわかります。
これにより、障害の発生点とそこに至るまでのすべてのステップが表示されます。呼び出し元が無効な値を渡すことが問題の場合があるため、これは便利です。
ここで Python を見てみましょう。
順序が逆になっていることに気づきましたか?ただし、提供される情報は、1 つの小さな違いを除いてほぼ同じです。 (Node.js のように) エラーが発生した関数を明確に示す代わりに、Python は呼び出しサイト (「1」) を表示し、エラーが発生した場所を示すために「3 つ」(注釈「2」) と表示します。これは読みにくいと思いますが、読みにくい人もいるかもしれません。
次に進みます。 Golang を見てみましょう。
Node.js スタック トレースのような、より整然とした均一な出力。注意すべき主な違いは、文字番号がないことですが、行番号は表示されます。
つまり、スタック トレースがどのように整理されるかは、使用しているコンパイラまたはランタイムによって異なります。ただし、一部のランタイム、コンパイラ、またはエコシステムはスタック トレースを無視するため、役に立たなくなります。次に、極端な例を示します。良いスタック トレースと悪いスタック トレースを混同したり、完全に放棄したりしないことを願っています。
一部のスタック トレースが読み取れない
それはあなただけではありません。それらは皆の士気を失わせますが、時にはそれらを修正できることもあります
スタック トレースを読むのをやめて、代わりに Google (現在は LLM) に問題を説明するのは非常に簡単です。これは、怠慢な設計を通じてプログラマに長い間訓練された動作です。私が言いたいのは、一部の言語 (Rust や Python など) は有益な情報の伝達を重視する一方で、そうでない言語もあるということです。 JavaScript などの言語の一般的な慣行により、スタック トレースを読み取ることができなくなります。バンドル、トランスパイル、縮小が主な原因であり、これらの各手順により有用な情報が削除されます。
たとえば、次のスタック トレースを考えてみましょう。これは TypeScript をトランスパイルし、Webpack と Terser を使用してバンドルおよび縮小されます。これは単に、複数ファイルの TypeScript プログラムを JavaScript の単一のチャンクに変換し、すべての有用な関数名と変数名を 1 つの文字に変更したことを意味します。
注釈「1」を見てください。プログラム全体が 1 行にあるため、「Object」以降の出現箇所はすべて「1」行にあることがわかります。注釈「2」を見ると、ここで問題が発生していますが、それでもナンセンスです。ランタイムはチャンクを指し、「そこのどこか」とさえ言います。これらは、ソース マップが設定されていない限り、ブラウザに表示される一般的なスタック トレースです。
ソース マップは、元のソース コードを指す個別の JSON ファイルで、トランスパイルされたコード内のコメントによって参照されます。問題が発生した場合、問題の原因を見つけるのがはるかに簡単になります。たとえば、ブラウザでは、クリックスルーして正確な TypeScript 行を表示することもできます。ただし、これらは通常、運用コードでは使用されず、気分的に不安定になることがよくあります。フロントエンド ビルド システムの複雑さはまったく別のトピックであり、複雑な要件がない限り、複雑なセットアップを避けるようアドバイスするだけで十分です。つまり、KISS です。
LLM を使用してデバッグを支援する
ゴミが入っています。解決策が出た
LLM (クロード コードなど) の優れた使用例の 1 つは、前の例のようにスタック トレースが読み取れない場合でも、スタック トレースを読み取ることです。クロードもソース コードにアクセスできれば、それらを問題なく理解できるでしょう。
クロードにスタック トレースを提供するには、スクリーンショットを提供するか、クロードにコマンドを直接実行させることができます。私は Emacs パッケージを書くために Claude をよく使ってきました (私の Emacs の知識には限界があるため)。これにより Elisp スタック トレースが実際に便利になり、大きな問題点が解消されました。
これは、想像力 (LLM の弱点) が必要なく、診断に必要なほとんどすべてがトレース内に存在するため、うまく機能します。クロード (つまりコンピューター) は私よりも桁違いに速くテキストを読むことができるため、その長所をそのまま発揮します。
デバッグ用の LLM はうまく機能します。ただし、クロードはまだ行き詰りやすいため、特効薬ではありません。
より良いプログラマーになる: 成長するための 7 つの習慣
より良いプログラムを書くための、歴戦の習慣。
要約してまとめてみましょう。
-
コール スタックは、ポインタとスタック フレームのデータ構造です。
-
スタック フレームは、関数 (またはメソッド) の状態を保存するために使用されるメモリの分離されたセグメントです。
-
スタック トレースは順番に並べられ、関数名、ファイル パス、行番号、場合によっては問題に至るまでのすべての呼び出しの文字番号が含まれます。
-
スタック トレースの最新の行は問題が発生した場所であり、(使用するランタイムまたはコンパイラによって異なります) 先頭または末尾にある可能性があります。
問題が発生するところから始めます。ほとんどの場合、信号は明確であり、問題は簡単に解決できます。破損した値が渡されている場合は、前の手順を確認する必要がある場合があります。
重要なポイントは次のとおりです。 失敗点の周囲を見回す そしてそれを理解してください ほとんどのスタック トレース情報はソース コード内の場所を指しています。。
フロントエンド コードの場合は、ソース マップを含み、縮小とバンドルをスキップする専用の開発ビルドを用意します。 Vite のような開発 Web サーバーはこのアプローチを採用し、開発モードで ES モジュール (プレーンで読み取り可能な JS) を提供しますが、実稼働ビルド用のコードをバンドルします。