Skip to content

A backtrace is data you can read

Level: 201 · a Python programmer who has used traceback and inspect and wants the Ruby names for the same frames

One line: caller returns an Array of Strings shaped file:line:in 'Klass#method', caller_locations returns the same frames as objects with label, path and lineno, and a rescued exception carries both as backtrace and backtrace_locations — the twins of traceback.extract_stack, inspect.stack and e.__traceback__, with 4.0's Klass#method labels matching Python's co_qualname.

Three sources of the same information. Inside a running method, caller (the callers, innermost first) and caller_locations(start, count) describe the stack right now; __method__ names the current method as a Symbol. A raised exception freezes that stack: e.backtrace is the string form and e.backtrace_locations the object form, both nil until the exception is actually raised. And e.full_message is what the interpreter prints for an uncaught exception — the first line holds the location, the message and the class — while e.detailed_message is the message with the class appended and, when error_highlight is loaded (it is, by default), a snippet of the failing source under it. The labels changed in 3.4: 'Worker#inner' instead of `inner', so a frame now says which class the method belongs to; base_label is the bare method name. The program masks the one line that carries a path and a line number, and reduces every other frame to its label with a regex, so the key is stable.

Verified output of backtraces_and_caller_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. caller, reduced to labels:            ["Worker#outer", "<main>"]
 2. caller_locations(0, 3) labels:        ["Worker#inner", "Worker#outer", "<main>"]
 3. ...and their base_labels:             ["inner", "outer", "<main>"]
 4. __method__ inside the method:         :inner
 5. a location has a path and a line:     backtraces_and_caller_rb.rb, lineno is Integer
 6. e.backtrace (Array) labels:      ["Worker#inner", "Worker#outer", "<main>"]
 7. e.backtrace_locations labels:         ["Worker#inner", "Worker#outer", "<main>"]
 8. full_message first line:              FILE:LINE:in 'Worker#inner': bad input (ArgumentError)
 9. detailed_message first line:          bad input (ArgumentError)   (error_highlight added a snippet below it: true)
10. after set_backtrace:                  backtrace ["custom:1:in 'made_up'"], backtrace_locations still the originals: true
11. an exception never raised:            RuntimeError.new("x").backtrace is nil
12. caller at the top level:              []
13. labels elsewhere:                     top-level def: "Object#top_level_method"   block: "block in <main>"   main: "<main>"

Reading the output

Rows 1–3 are the live stack seen from Worker#inner: caller lists its callers (Worker#outer, then <main>), caller_locations(0, 3) starts at the current frame and adds it, and base_label strips the class. Row 4 is __method__. Row 5 shows a location's path (the program's own file, printed by basename) and that lineno is an Integer. Rows 6 and 7 are the same three frames captured by the exception. Row 8 is the first line of full_message — FILE:LINE:in 'Worker#inner': bad input (ArgumentError) — the exact line an uncaught exception prints. Row 9 is detailed_message: the message with the class name in parentheses, and true because error_highlight appended the source snippet. Row 10 is set_backtrace, which replaces the strings but, given strings, does not touch backtrace_locations — a subtlety worth knowing when a library rewrites backtraces. Row 11: a new exception has no backtrace until raised. Row 12: at the top level caller is empty. Row 13: a top-level def is Object#top_level_method, a block is block in <main>, and the main program is <main>.

For stopping inside a frame rather than reading it: binding.irb on any line opens an irb session there, and the bundled debug gem gives binding.break (alias debugger) with rdbg as its command-line front end, with step, next, up, info locals and breakpoints by file:line (Not machine-checked here.).

Compared with Python

Python's frames are objects from the start. traceback.extract_stack() returns the stack outermost first (row 1 reads ['<module>', 'outer', 'inner'] where Ruby's caller read innermost first), and inspect.stack() innermost first like caller_locations (row 2). A code object's co_qualname (3.11+) is Worker.inner, the twin of the Worker#inner label, and co_name is the base_label (row 3); sys._getframe().f_code.co_name is the __method__ idiom (row 4). Row 5: a FrameSummary has filename and lineno. An exception carries its traceback as e.__traceback__, a traceback object that traceback.extract_tb turns into the same frame summaries (rows 6–7). There is no full_message: format_exception_only gives the last line, ValueError: bad input, with no location in it (row 8), and 3.11's add_note appends lines to it (row 9), the closest thing to detailed_message. with_traceback(None) (row 10) is set_backtrace's counterpart. Rows 11 and 12 match Ruby: an unraised exception has None, and the module-level stack is just <module>. Row 13: a lambda's co_qualname is <lambda>. The full traceback text is deliberately never printed here — 3.13 added colours and carets whose exact characters differ between 3.12 and 3.14. breakpoint() (a hook into pdb by default) and python3 -m pdb script.py are the debugger side (Not machine-checked here.).

Verified output of backtraces_and_caller_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. extract_stack names (outermost first): ['<module>', 'outer', 'inner']
 2. inspect.stack names (innermost first): ['inner', 'outer', '<module>']
 3. co_qualname and co_name:                'Worker.inner', 'inner'
 4. f_code.co_name of the current frame:    'inner'
 5. a frame has a filename and a line:      backtraces_and_caller_py.py, lineno is int
 6. e.__traceback__ is a traceback; its names: ['<module>', 'outer', 'inner']
 7. extract_tb gives frames with .name:      ['<module>', 'outer', 'inner']
 8. format_exception_only:                   ['ValueError: bad input\n']   (no location at all)
 9. after add_note (3.11+):                  ['ValueError: bad input\n', 'checked by the twin\n']
10. after with_traceback(None):             e.__traceback__ is None
11. an exception never raised:              ValueError('x').__traceback__ is None
12. extract_stack at module level:          ['<module>']
13. names elsewhere:                        top-level def: 'top_level_function'   lambda: '<lambda>'   module: '<module>'

Try it

  1. Write who_called_me, which returns the label of whatever called it (caller_locations(1, 1)). Call it from an instance method, from inside a block in an instance method, and from the top level; print the three answers and notice the block in prefix.
  2. Rescue a ZeroDivisionError raised three methods deep, then print e.backtrace_locations as a table: one row per frame with base_label, label and whether path equals __FILE__ — and count how many frames belong to your own file.
Solution to 1

backtraces_and_caller_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: `who_called_me` answers with the label of whatever called it -- a
# method, a block inside a method, or the main program.
def who_called_me
  caller_locations(1, 1).first.label
end

class Report
  def build = who_called_me
  def each_line = [1, 2].map { who_called_me }
end

puts "1. from a method:     #{Report.new.build}"
puts "2. from a block:      #{Report.new.each_line.inspect}"
puts "3. from the top:      #{who_called_me}"

Verified output of backtraces_and_caller_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. from a method:     Report#build
2. from a block:      ["block in Report#each_line", "block in Report#each_line"]
3. from the top:      <main>

See also