Things You Didn't Know About Exceptions

Avdi Grimm (@avdi / avdi.org)

1. Retry

If at first you don't succeed…

Using a loop

tries   = 0
success = false
while(!success && tries < 3) 
  puts "Trying #{tries}..."
  return if success
  tries += 1
end
puts "Giving up."
Trying 0...
Trying 1...
Trying 2...
Giving up.

Using retry

tries = 0
begin
  tries += 1
  puts "Trying #{tries}..."
  raise "Didn't work"
rescue
  retry if tries < 3
  puts "I give up"
end
Trying 1...
Trying 2...
Trying 3...
I give up

More?

2. The $! global

What the heck is $!?

require 'English'
puts $!.inspect
begin
  raise "Oops"
rescue
  puts $!.inspect
  puts $ERROR_INFO.inspect
end
puts $!.inspect
nil
#<RuntimeError: Oops>
#<RuntimeError: Oops>
nil

A simple crash logger

at_exit do
  if $!
    open('crash.log', 'a')  do |log|
      error = {
        :timestamp => Time.now,
        :message   => $!.message,
        :backtrace => $!.backtrace,
        :gems      => Gem.loaded_specs.inject({}){
          |m, (n,s)| m.merge(n => s.version)
        }
      }
      YAML.dump(error, log)
    end
  end
end

Nested Exceptions

require 'English'
class MyError < StandardError
  attr_reader :original
  def initialize(msg, original=$ERROR_INFO)
    super(msg)
    @original = original
  end
end

Using nested exceptions

begin  
  begin
    raise "Error A"
  rescue => error
    raise MyError, "Error B"
  end
rescue => error
  puts "Rescued: #{error.inspect}"
  puts "Original: #{error.original.inspect}"
end
Rescued: #<MyError: Error B>
Original: #<RuntimeError: Error A>

More?

3. Re-raising

Playing Hot Potato with exceptions.

Failures without context

data = []
open('datafile') do |f|
  data << eval(f.gets) until f.eof?
end
SyntaxError: (eval):1:in `irb_binding': compile error
(eval):1: syntax error, unexpected $undefined

Adding context

data = []
open('datafile') do |f|
  begin
    data << eval(f.gets) until f.eof?
  rescue Exception => e
    raise e, "[At #{f.path}:#{f.lineno}] #{e.message}"
  end
end
SyntaxError: [At datafile:3] (eval):1:in `irb_binding': compile error
(eval):1: syntax error, unexpected $undefined

More?

4. raise is a method

Get out your best monkeypatching hammer!

Overriding raise

Make exceptions instantly fatal:

module RaiseExit
  def raise(msg_or_exc, 
            msg=msg_or_exc, 
            trace=caller)
    warn msg.to_s
    exit! 1
  end
end

class Object
  include RaiseExit
end

Hammertime

An error console for ruby: http://github.com/avdi/hammertime

require 'hammertime'
raise "Oh no!"
=== Stop! Hammertime. ===
    An error has occurred at example.rb:2:in 
`raise_runtime_error'
    The error is: #<RuntimeError: Oh no!>
    1. Continue (process the exception normally)
    2. Ignore (proceed without raising an exception)
    [...]
    5. Backtrace (show the call stack leading up to 
the error)
    6. Debug (start a debugger)
    7. Console (start an IRB session)

More?

5. #exception()

Like #to_s only for exceptions.

How you think raise works

def raise(klass, message, backtrace)
  error = klass.new(message) # Nope!
  # ...
end

How raise really works

def raise(error_class_or_obj, message, backtrace)
  error = error_class_or_obj.exception(message)
  # ...
end

.exception()

  • Exception.exception
    Equivalent to calling Exception.new().
  • Exception#exception
    • With no arguments, returns self.
    • With a message, returns a new exception.

Implement your own #exception

class Net::HTTPResponse
  def exception(message="HTTP Error")
    RuntimeError.new("#{message}: #{code}")
  end
end

# ...
response = Net::HTTP.get_response(url)
raise response
exception_method.rb:11: HTTP Error: 404 (RuntimeError)

More?

6. Dynamic rescue clauses

Getting picky about what to rescue.

rescue and case

# case
case obj
when Numeric, String, NilClass, FalseClass, TrueClass
  puts "scalar"
# ...
end

# rescue
rescue SystemCallError, IOError, SignalException 
  # handle exception...
end

Dynamic exception lists

def ignore_exceptions(*exceptions)
   yield
 rescue *exceptions => e
   puts "IGNORED: '#{e}'"
 end

 puts "Doing risky operation"
 ignore_exceptions(IOError, SystemCallError) do
   open("NOTEXIST")
 end
 puts "Carrying on..."
Doing risky operation
IGNORED: 'No such file or directory - NOTEXIST'
Carrying on...

More?

7. Really dynamic rescue clauses

Dynamic matcher generator

def errors_matching(&block)
  m = Module.new
  (class << m; self; end).instance_eval do
    define_method(:===, &block)
  end
  m
end

Generating a matcher dynamically

begin
  raise RetryableError.new("Connection timeout", 2)
rescue errors_matching{|e| e.num_tries < 3} => e
  puts "Ignoring #{e.message}"
end

More?

8. SystemExit

It's the end of the program as we know it.

exit

begin
  puts "Terminating program..."
  exit
rescue Exception => e
  puts "Rescued: #{e.inspect}, status: #{e.status}"
end
puts "Continuing on..."
Terminating program...
Rescued: #<SystemExit: exit>, status: 0
Continuing on...

abort

begin
  abort "Aborting..."
rescue Exception => e
  puts "Rescued: #{e.inspect}, status: #{e.status}"
end
Rescued: #<SystemExit: Aborting...>, status: 1

exit!

begin
  puts "Terminating for real"
  exit!
rescue Exceptions => e
  # will never get here
  # no crash logger either
  puts "Rescued: #{e.inspect}"
end

More?

9. Ignorable exceptions

LA LA LA I CAN'T HEAR YOU

A whiny method

def whiny_method
  puts "Before raising"
  raise "Pay attention to me!"
  puts "After raising"
end

Ignoring the whiny method

begin
  whiny_method
rescue => e
  puts "Ignoring '#{e}'"
  e.ignore
end
Before raising
Ignoring 'Pay attention to me!'
After raising

What?

OK, maybe not out of the box.

Exception with continuation

class Exception
  attr_accessor :continuation
  def ignore
    continuation.call
  end
end

Capturing the continuation

require 'continuation' # Ruby 1.9
module RaiseWithIgnore
  def raise(*args)
    callcc do |continuation|
      begin
        super
      rescue Exception => e
        e.continuation = continuation
        super(e)
      end
    end
  end
end

Override system raise

class Object
  include RaiseWithIgnore
end

Using Exception#ignore

begin
  whiny_method
rescue => e
  puts "Ignoring '#{e}'"
  e.ignore
end
Before raising
Ignoring 'Pay attention to me!'
After raising

More?

Just kidding.

Thank You

exceptional-ruby-cover-small.png

  • Book!
    • exceptionalruby.com
    • Code: ROCKYMTNRUBY
  • Contact!
    avdi.org / @avdi
  • Favor!
    Tweet "Thank you @taboulichic!"