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 $+

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
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required