APM Academy

How to Read Ruby Stack Traces: Backtraces, Causes and Fixes

How to Read Ruby Stack Traces: Backtraces, Causes and Fixes

A Ruby stack trace lists the method calls that led to an exception, most recent call first. Each line shows a file, line number, and method: divide.rb:3:in 'Object#divide'. Ruby 3.4 prints the trace top-down, underlines the failing call, and suggests fixes for typos. Read from the top line downward to find where the error started.

When an error happens in your application, Ruby raises an exception and prints the stack trace to the terminal or log. This article reads that trace line by line: the frame format Ruby 3.4 prints, the highlighting and suggestions that travel with it, and how to walk from the error back to its source. Every output block comes from Ruby 3.4.

The Call Stack

Whenever you call a method, Ruby places a stack frame on the call stack (the “runtime stack”, usually shortened to “the stack”). The stack frame is a memory allocation that holds the method’s arguments, some space for internal variables, and the return address of the caller.

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

When one method (divide) calls another (Integer#/, or / for short), Ruby puts the called method on top of the stack — it has to finish before its caller can. When calling divide(8, 4) in this example, Ruby executes the following actions in order:

  1. Call 8./(4)
  2. Collect the division’s result (2), and put it in a string
  3. Collect the string ("Dividing 8 by 4 gives 2."), and print it to the console with puts

Each call pushes a frame onto the stack, and each return pops one off. A stack trace is a snapshot of those frames at one moment.

Stack Traces

The stack trace (usually named “backtrace” in Ruby, but also referred to as “stack backtrace” and “stack traceback”) is a human-readable representation of the call stack at a specific moment while running your program. While the / method runs, the stack holds three frames, from the most recent call down to the program’s entry point:

Shell
divide.rb:3:in 'Integer#/'
divide.rb:3:in 'Object#divide'
divide.rb:6:in '<main>'

Calling divide with a 0 as its second argument raises a ZeroDivisionError exception:

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

When that happens, Ruby prints the exception, along with the stack trace, to the console:

Shell
$ ruby divide.rb
divide.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)
	from divide.rb:3:in 'Object#divide'
	from divide.rb:6:in '<main>'

Ruby prints the most recent call first: the top line is where the exception was raised, and every following line steps one call outward. (Ruby 2.5 experimented with the reverse order; Ruby 3.0 reverted it, and Ruby 3.4 prints top-down in both terminal and log output.) Line by line:

  1. The first line names the file and line number the exception was raised on (divide.rb:3), the method it was raised in ('Integer#/' — the receiver’s class and the method name, in straight quotes; frames used to read in `/', without the class), and the exception itself: “divided by 0” as the message, ZeroDivisionError as the class.

  2. The second line shows where the / method was called from: our divide method, on line 3. The from prefix marks every frame after the first.

  3. The last line shows that divide was called from <main>, which refers to the initial context of a Ruby program — code running outside any method.

What a Modern Ruby Stack Trace Shows

The frames are only part of what Ruby prints. Depending on the exception, a Ruby 3.4 trace also carries an underlined copy of the failing source line, a spelling suggestion, and a second trace for the exception’s cause. Each piece has its own job when you’re reading one.

The Ruby 3.4 NoMethodError Format

The exception you’ll trace most often is a NoMethodError on nil:

Ruby
# nil_method.rb
product = nil
product.name
Shell
nil_method.rb:3:in '<main>': undefined method 'name' for nil (NoMethodError)

product.name
       ^^^^^

The message format changed in Ruby 3.4. Older versions printed undefined method `name' for nil:NilClass, with a backquote-quote pair around the method name and the receiver’s class appended. Ruby 3.4 uses straight quotes and describes the receiver plainly: for nil when the receiver is nil, and “for an instance of String” — rather than an inspect dump of the object — for anything else. If you grep logs or match alerting rules against these messages, the new format is the one to match. The two extra lines under the message, the source line and the carets, come from error highlighting, covered in its own section.

Did You Mean? Method Suggestions

When the missing method’s name is one typo away from a method that exists, the trace includes a suggestion:

Ruby
# typo.rb
"hello".uppcase
Shell
typo.rb:2:in '<main>': undefined method 'uppcase' for an instance of String (NoMethodError)

"hello".uppcase
       ^^^^^^^^
Did you mean?  upcase
               upcase!

The suggestions come from the did_you_mean gem, which has shipped with Ruby since 2.3. It compares the missing name against the methods the receiver does respond to and lists the closest matches — here, both upcase and upcase!. It works for misspelled constants, variables, and hash keys as well.

The Underlined Call: Error Highlighting

The caret lines come from the error_highlight gem, enabled by default since Ruby 3.1. On a line with a single call, it underlines that call. It earns its keep on a chained line, where it isolates the one call that raised:

Ruby
# highlight.rb
data = { name: "Alice" }
puts data[:nam].upcase
Shell
highlight.rb:3:in '<main>': undefined method 'upcase' for nil (NoMethodError)

puts data[:nam].upcase
               ^^^^^^^

The carets sit under .upcase, not [:nam]: the hash lookup succeeded — returning nil, because the key is misspelled — and the exception was raised by calling upcase on that nil. Before error highlighting, an “undefined method for nil” on a chained line left you to work out which step in the chain produced the nil.

Exception Causes: Two Traces in One

When an exception is raised inside a rescue block — wrapping a low-level error in a domain-specific one, for example — Ruby stores the original exception as the new one’s cause and prints both:

Ruby
# cause2.rb
class ImageDownloadError < StandardError; end
 
def download_image(url)
  raise ArgumentError, "no image URL given"
rescue ArgumentError
  raise ImageDownloadError, "image could not be downloaded"
end
 
download_image(nil)
Shell
cause2.rb:7:in 'Object#download_image': image could not be downloaded (ImageDownloadError)
	from cause2.rb:10:in '<main>'
cause2.rb:5:in 'Object#download_image': no image URL given (ArgumentError)

  raise ArgumentError, "no image URL given"
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
	from cause2.rb:10:in '<main>'

There is no “Caused by” label: the wrapping ImageDownloadError comes first with its trace, and the original ArgumentError follows with its own trace (and its own highlight). When you rescue the wrapping error, Exception#cause hands you the original programmatically:

Ruby
begin
  download_image(nil)
rescue ImageDownloadError => e
  e.cause.class    # => ArgumentError
  e.cause.message  # => "no image URL given"
  e.cause.cause    # => nil
end

The chain continues until cause returns nil, so a deeply wrapped exception keeps its full history.

A stack trace in your terminal is one thing; a stack trace from a production request you can no longer replay is another. AppSignal’s Ruby error tracking stores the full backtrace, the cause chain, and the request context for every exception, so you can walk the stack for errors you never saw locally.

For how those production traces get reported and routed in the first place, see error reporting for Rails exceptions.

backtrace, full_message, and the Traceback Block

Everything so far is what Ruby prints when a program crashes. When you rescue an exception yourself, you decide what to log, and the exception object offers two views of the stack.

Exception#backtrace returns the frames as an array of location strings, with no message, class, or highlighting attached:

Ruby
e.backtrace
# => ["full_message.rb:3:in 'Integer#/'", "full_message.rb:3:in 'Object#divide'", "full_message.rb:7:in '<main>'"]

Exception#full_message returns the complete report as one string — message, class, backtrace, highlighting, and any cause — formatted the way an uncaught exception is printed. It takes two keyword arguments: highlight: toggles the ANSI escape codes, and order: picks the direction. Passing order: :bottom puts the message last and the outermost frame first:

Ruby
# full_message.rb
def divide(a, b)
  a / b
end
 
begin
  divide(8, 0)
rescue ZeroDivisionError => e
  puts e.full_message(highlight: false, order: :bottom)
end
Shell
Traceback (most recent call last):
	2: from full_message.rb:7:in '<main>'
	1: from full_message.rb:3:in 'Object#divide'
full_message.rb:3:in 'Integer#/': divided by 0 (ZeroDivisionError)

This reversed block — with the “Traceback (most recent call last):” header Ruby 2.5 briefly used for all output — now appears only when you request it. Everywhere else, top-down is the rule. When an application rescues and logs exceptions like this, make sure they still reach your error tracker — AppSignal’s exception handling documentation shows how to report exceptions you rescue yourself.

Understanding Stack Traces

Although the first line of the stack trace shows the line the exception occurred on, it doesn’t always show the source of the error. In the divide-by-zero example, line 3 did its job correctly; it couldn’t handle the data passed to the divide method. Walking down the trace leads to the call site — divide(8, 0) on line 6 — which is the source of the problem. That’s where the fix belongs: in the caller that passed the bad data, or in divide learning to handle a zero.

The same walk works in a Rails request, with framework frames layered between yours; Debugging Exceptions in Rails traces a Rails exception end-to-end. The reading order is the same everywhere: the top line tells you what was raised and where, the carets point at the failing call, a second trace names the original cause, and the frames underneath lead to the caller that set it all in motion.

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Frequently asked questions

How do I read a Ruby stack trace?
Start at the top line, which names the file, line number, and method where the exception was raised, plus the error class and message. Each following line is one step outward in the call chain. Walk down until you reach code you wrote.
Is a Ruby stack trace printed top-down or bottom-up?
Ruby 3.4 prints the most recent call first: the error line at the top, callers underneath. The reversed format with a Traceback header, introduced in Ruby 2.5, was reverted in Ruby 3.0 and now only appears when you request it from full_message.
What is the difference between backtrace and full_message in Ruby?
backtrace returns an array of location strings for the exception, with no message attached. full_message returns the complete formatted report as one string: message, error class, backtrace, terminal highlighting, and any cause. Use full_message when logging an exception you rescued yourself.
What does error highlighting do in Ruby?
Since Ruby 3.1, the error_highlight gem underlines the exact part of the line that failed, such as the method call that returned nil in a chain. It runs by default for NoMethodError and similar errors, so the trace points at the failing call, not the whole line.
How do I see what caused an exception in Ruby?
When an exception is raised inside a rescue block, Ruby stores the original error in cause and prints both traces together, the wrapping error first. Call cause on the rescued exception to inspect the original programmatically; the chain continues until cause returns nil.

Published , Updated

Wondering what you can do next?

  • Share this article on social media

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal