Working with Ruby exceptions outside of the rescue block

It's often useful to be able to get the most recent exception, even if your code doesn't control the lifecycle of that exception. In this post we explore a few of the ways to do this.

It's often useful to be able to get the most recent exception, even if your code doesn't control the lifecycle of that exception. Imagine that you want to add basic crash detection to your application. You'd like to log extra info about any crash that happens as a result of an uncaught exception.

The first step is to add a handler that's run whenever your application exits. It's super easy to do this via the Ruby kernel's at_exit method.

at_exit
  puts "the app exited"
end

But how can we know if the exit callback was invoked as a result of an exception? Well, Ruby provides the cryptically named $! global variable. It contains the most recently raised exception that has occurred somewhere in the current call stack.

It's trivial to use $! to detect if the program is being exited due to an exception. It looks something like this:

at_exit do
 save_error_to_log($!) if $!         
end

The limitations of $!

Unfortunately, the $! method only works if the exception occurred somewhere in the current call stack. If you rescue an exception, then try to access $! outside of the rescue clause, you'll get nil.

begin        
 raise "x"       
rescue       
 puts $! # => RuntimeError           
end

puts $! # => nil         

This means that $! is pretty useless inside of a shell like IRB. Often in IRB, I'll run a method and get an exception. Sometimes I'd like to get ahold of that exception object. But $! doesn't work for this.

irb(main):001:0> 1/0
ZeroDivisionError: divided by 0
    from (irb):1:in `/'
irb(main):002:0> $!
=> nil

Working around $! with PRY

PRY gets around the limitations of $! by adding its own local variable, _ex_. This variable contains the most recent uncaught exception.

[1] pry(main)> raise "hi"        
RuntimeError: hi         
from (pry):1:in `__pry__'        
[2] pry(main)> _ex_      
=> #<RuntimeError: hi>

The reason that PRY is able to do this is because there are not really any uncaught exceptions inside of PRY or IRB. The shell itself catches the exceptions and displays them as nicely-formatted error messages.

I've copied the relevant bits of the PRY source below. You can see that the code that evaluates your commands is wrapped inside of a begin/rescue/end block. When a rescuable exception occurs, PRY saves the exception to self.last_exception and it later gets assigned to _ex_.

# Excerpted from the PRY source at https://github.com/pry/pry/blob/623306966bfa86890ac182bc8375ec9699abe90d/lib/pry/pry_instance.rb#L273

begin
  if !process_command_safely(line)
    @eval_string << "#{line.chomp}\n" if !line.empty? || !@eval_string.empty?
  end
rescue RescuableException => e
  self.last_exception = e
  result = e

  Pry.critical_section do
    show_result(result)
  end
  return
end

Require English

Perhaps you find variable names like $! a little hard on the eyes? Fortunately, Ruby includes a module called "English" which provides english-language versions of many global variables which otherwise look like robot cusswords.

The synonym for $! is $ERROR_INFO. You can use it wherever you'd normally use $!.

require "English"

begin        
 raise "x"       
rescue       
 puts $ERROR_INFO # => RuntimeError          
end

And although most of the other english equivalents have nothing whatsoever to do with the topic of this blog post, I'm including them for kicks. English variables are on the left. The originals are on the right.

$ERROR_INFO $!
$ERROR_POSITION $@
$FS $;
$FIELD_SEPARATOR $;
$OFS $,
$OUTPUT_FIELD_SEPARATOR $,
$RS $/
$INPUT_RECORD_SEPARATOR $/
$ORS $\
$OUTPUT_RECORD_SEPARATOR $\
$INPUT_LINE_NUMBER $.
$NR $.
$LAST_READ_LINE $_
$DEFAULT_OUTPUT $>
$DEFAULT_INPUT $<
$PID $$
$PROCESS_ID $$
$CHILD_STATUS $?
$LAST_MATCH_INFO $~
$IGNORECASE $=
$ARGV $*
$MATCH $&
$PREMATCH $`
$POSTMATCH $‘
$LAST_PAREN_MATCH $+

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial