Filtering Ruby Backtraces for Debugging

Anuj Biyani
One Medical Technology
3 min readDec 5, 2022
Photo by X on Unsplash

Backtraces are an exceptionally useful piece of data for debugging an issue, and every error-tracking tool (e.g. Rollbar or Honeybadger) supports them quite nicely. In most application development, we like to hide these traces from end users and instead rely on the error-tracking tool to send them to developers.

When building a tool for internal consumption, however, I’ve found that it’s often better to surface a backtrace in the UI than to resort to the error logger. Then, if another developer in your company encounters an error while using your tool, the backtrace will empower them to fix the issue themselves.

I built some rather complicated batching logic recently for One Medical that allowed an office manager to, for example, cancel hundreds of appointments at once. It doesn’t happen often, but occasionally an office has to urgently close (in the case of an extended power outage, etc.), and canceling those appointments individually is tedious. Any appointment in that batch could fail to process for a number of reasons, so we wanted some tidy error handling. The default approach would be to let the error logger handle the backtrace, but instead I opted to collate all backtraces, store them in a database, and present them to the internal user. That led to a much quicker debugging process whenever issues arose.

Except for one problem: those backtraces were noisy. It’s easy enough to get a backtrace in Rails. Anytime you call raise and then rescue => error, you can access the backtrace with error.backtrace. But, if you’ve ever looked at those traces, you’ll notice a lot of cruft in there that makes the raw text essentially useless. There are way too many lines from gems or Rails to sift through while looking for the lines belonging to your feature.

Fortunately, Rails actually provides a very nice tool for this exact issue: a backtrace cleaner. Here’s how to use it:

begin
code_that_might_blow_up
rescue => error
Rails.backtrace_cleaner.clean(error.backtrace)
end

Here’s a backtrace I generated, without cleaning:

/Users/username/code/my_project/app/models/user.rb:559:in `raw_backtrace'
/Users/username/code/my_project/app/models/user.rb:549:in `some_function'
/Users/username/code/my_project/spec/models/user_spec.rb:36:in `block (2 levels) in <top (required)>'
/Users/username/.asdf/installs/ruby/2.7.2/lib/ruby/gems/2.7.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:263:in `instance_exec'
/Users/username/.asdf/installs/ruby/2.7.2/lib/ruby/gems/2.7.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:263:in `block in run'
/Users/username/.asdf/installs/ruby/2.7.2/lib/ruby/gems/2.7.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'
[… 40+ more lines …]

And with cleaning:

app/models/patient.rb:553:in `cleaned_backtrace'
app/models/patient.rb:549:in `some_function'

That’s it! The default cleaner will remove most of the noise from your backtrace. You can also add additional filters (which mutate lines in your backtrace) or silencers (which remove lines from your backtrace) if you want to further customize your cleaner. Here’s an example taken from Rails’ documentation:

bc = ActiveSupport::BacktraceCleaner.new
bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
bc.clean(exception.backtrace) # perform the cleanup

If you prefer to still use your error logging tool, you can actually take this cleaned-up backtrace and manually report it, then share a URL to that error with your end user for the best of both worlds.

Hope this helps you write more debuggable code!

--

--