Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのスタックトレースを読み解く:例外の発生箇所を特定する方法

アプリケーションでエラーが発生すると、Rubyは例外を発生させ(raise)、ログにスタックトレースを出力します。この記事では、スタックトレースの基本的な読み方と、それを活用してアプリケーション内の例外の発生源を特定する方法を解説します。

コールスタックとは

メソッドを呼び出すたびに、Rubyはコールスタック(「ランタイムスタック」とも呼ばれ、単に「スタック」と略されることが多いです)にスタックフレームを積み上げます。スタックフレームとはメモリ上の領域のことで、メソッドの引数、内部変数用のスペース、そして呼び出し元へのリターンアドレスなどを保持しています。

# divide.rb
def divide(a, b)
  "Dividing #{a} by #{b} gives #{a / b}."
end
 
puts divide(8, 4)

あるメソッド(divide)が別のメソッド(Fixnum#/、略して/)を呼び出す場合、後者は先に実行を完了する必要があるため、スタックの一番上に積まれます。この例でdivide(8, 4)を呼び出すと、Rubyは以下の順序で処理を実行します。

  1. 8./(4) を呼び出す
  2. 除算の結果(2)を受け取り、文字列に埋め込む
  3. 完成した文字列("Dividing 8 by 4 gives 2")を受け取り、putsでコンソールに出力する

スタックトレースとは

スタックトレース(Rubyでは通常「バックトレース」と呼ばれ、「スタックバックトレース」「スタックトレースバック」と表記されることもあります)とは、プログラム実行中の特定の時点におけるスタックの状態を、人間が読める形式で表現したものです。/メソッド呼び出し時のスタックトレースは次のようになります。2行目のdivideメソッド内から呼び出され、さらにそのdivideは5行目の<main>から呼び出されたことがわかります。

divide.rb:2:in `/'
divide.rb:2:in `divide'
divide.rb:5:in `<main>'

先ほどの例で、divideメソッドの引数の1つに0を渡すと、ZeroDivisionError例外が発生します。

# divide_by_zero.rb
def divide(a, b)
  "Dividing #{a} by #{b} gives #{a / b}."
end
 
puts divide(8, 0)

例外が発生すると、Rubyは例外の内容とスタックトレースをコンソールに出力します。スタックトレースにはスタックの状態が人間が読める形式で表示され、各メソッドがコード上のどの位置にあったかが示されるため、例外の発生元を突き止める大きな手がかりになります。

$ ruby divide_by_zero.rb
divide_by_zero.rb:2:in `/': divided by 0 (ZeroDivisionError)
        from divide_by_zero.rb:2:in `divide'
        from divide_by_zero.rb:5:in `<main>'
  1. スタックトレースの1行目を見ると、メッセージ「divided by 0」を伴ってZeroDivisionErrorが発生したことがわかります。また、divide_by_zero.rb:2:in `/'とあることから、エラーはサンプルファイルの2行目、/という名前のメソッド(最初の引数がFixnumの8であるため、Fixnum#/)から発生したことが読み取れます。

  2. 2行目は、/メソッドがどこから呼び出されたかを示しています。この場合は、同じく2行目にあるdivideメソッドからの呼び出しです。

  3. 最終行は、divide<main>から呼び出されたことを示しています。<main>はRubyアプリケーションの初期コンテキストを指し、一般には「実際の」メソッドの外側、つまりスクリプトのトップレベルから呼び出されたことを意味します。

補足:Ruby 2.5以降では、ターミナルの画面に収まりやすいよう、ロガーはスタックトレースを逆順に出力します。最終行に例外の内容が表示され、その直前に例外が発生した行が続きます。それより上の行は、スタックを遡る呼び出し経路を表しています。

スタックトレースを正しく理解する

スタックトレースは、例外が発生した瞬間のコールスタックの状態をダンプしたものであり、「どこで問題が起きたのか」を突き止めるための強力な手がかりとなります。

ただし、スタックトレースの1行目に表示されるのはあくまで「例外が発生した行」であり、必ずしもエラーの根本原因を示しているわけではありません。先ほどの例では、プログラム自体は正しく動作していましたが、divideメソッドに渡されたデータ(0による除算)を処理できなかったのです。スタックトレースを上へ遡って呼び出し元を確認することで、初めて問題の真の発生源にたどり着けます。

この記事が役立ったかどうか、疑問点や次に読みたいテーマなどがあれば、ぜひ@AppSignalまでフィードバックをお寄せください。

  1. Rbenv・RubyGems・Bundlerの仕組みと連携を徹底解説

    Rubyで開発を行う際、依存関係の管理は通常、プロジェクトが依存するRubyのバージョンやgemのバージョンを明示することから始まります。筆者の経験上、Rubyの依存関係のデバッグは最も苦労する作業のひとつでした。多くの処理は「そのまま動く」ため失敗は頻繁ではありませんが、一度問題が発生すると、原因の特定と修正が不必要に難しくなることが多いのです。本記事では、Rubyにおける依存関係管理に関わる各コンポーネントを整理して解説します。これにより、奇妙な問題に直面した際のデバッグが格段にしやすくなるでしょう。 Rubyのコード読み込みの仕組み デフォルトで、Ruby言語には外部で定義された

  2. Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説

    ※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す