Exceptional Ruby

Avdi Grimm (@avdi / avdi.org)

What is a failure?

  • Bertrand Meyer: every method has a contract
  • Either implicit or explicit
  • The Eiffel influence

The Definition of Failure

A failure occurs when code is unable to fulfill its contract.

  • A mistake in usage
    User.find(nil)
    
  • A mistake in the code
    h = {:count => 42}; h["count"] += 1
    

The Definition of Failure cont'd

  • An unexpected case
    case message_type
    when "success" then # ...
    when "failure" then # ....
    else # uh oh...
    end
    
  • Failure of an external element
    HTTP.get_response(url).code # => 500
    

Lifecycle of an Exception

If code in one routine encounters an unexpected condition that it doesn't know how to handle, it throws an exception, essentially throwing up its hands and yelling "I don't know what to do about this–I sure hope somebody else knows how to handle it!"

Code Complete

Raising Exceptions

batman-small.jpg

raise (or fail)

  • Every exception starts with a raise (or fail)
  • Which to use?

    I almost always use the "fail" keyword… the only time I use "raise" is when I am catching an exception and re-reaising it, because here I'm not failing, but explicitly and purposefully raising an exception. – Jim Weirich

    begin
     fail "Oops";
    rescue => error
      raise if error.message != "Oops"
    end
    

raise with no arguments

raise

… is equivalent to:

raise RuntimeError

raise with a string

Raises a RuntimeError with the string as its message.

raise "Doh!"

… is equivalent to:

raise RuntimeError, "Doh!"

raise with an error class

Raises an object of the specified class.

raise ArgumentError, "Doh!"

raise with a custom backtrace

1:  def assert(value)
2:    raise(RuntimeError, "Doh!", caller) unless value
3:  end
4:  assert(4 == 5)

…results in:

set_backtrace.rb:4: Failed (RuntimeError)

raise is a method on Kernel

A method, not a keyword.

This means we can override it!

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)
    What now?

raise internals

raise SomeError, "Bad stuff"
  1. Get the excepion object.
  2. Set the backtrace.
  3. Set the global error info variable.
  4. Start unwinding the call stack.

Step 1: Get the exception object

You might think raise does this:

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

But in fact it does this:

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)

Step 2: Set the backtrace

error.set_backtrace([...])

Step 3: Set global error variable

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

Step 4: Unwind the call stack

Until it hits a rescue, ensure, or exits the program.

rescue

In practice, the rescue clause should be a short sequence of simple instructions designed to bring the object back to a stable state and to either retry the operation or terminate with failure.

Object Oriented Software Construction

rescue with no arguments    Incremental

rescue
  # ...
end
  • Equivalent to…
    rescue StandardError
      # ...
    end
    
  • Won't catch:
    • NoMemoryError
    • LoadError
    • NotImplementedError
    • SignalException
    • Interrupt
    • And others…

rescue with just a name

rescue => error
  # ...
end

…is equivalent to…

rescue StandardError => error
  # ...
end

rescue with a class

Only catches errors of that class.

rescue SomeError => error
  # ...
end

Also accepts a list of classes:

rescue SomeError, SomeOtherError => error
  # ...
end

rescue with a dynamic matcher

def errors_with_message(pattern)
  m = Module.new
  (class << m; self; end).instance_eval do
    define_method(:===) do |e|
      pattern === e.message
    end
  end
  m
end

puts "About to raise"
begin
  raise "Timeout while reading from socket"
rescue errors_with_message(/socket/)
  puts "Ignoring socket error"
end

ensure

begin
  raise "Uh oh"
ensure
  # will always get here
end
  • Always executed
  • Good place for required cleanup

ensure with explicit return

An unexpected edge case documented by Les Hill:

begin
  raise Exception, "Very bad!"
ensure
  return "A-OK"
end
A-OK

Ensure with an explicit return will eat any exception as if it had been caught and thrown away!

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

raise during exception handling

parachute.jpg

raise a new error

begin
  raise "Error A"
rescue
  raise "Error B"
end
  • No way to discover original failure
  • Rails does this a lot
  • Please don't do this

Nested Exceptions

class MyError < StandardError
  attr_reader :original
  def initialize(msg, original=$!)
    super(msg)
    @original = original
  end
end
# ...
rescue => error
  raise MyError.new("Error B", error)
end
# "Magic" original detection:
rescue
  raise MyError, "Error B"
end

Re-raise with the caught error

Re-raises the same object again.

begin
  begin
    raise "Error A"
  rescue => error
    puts error.object_id.to_s(16)
    raise error
  end
rescue => error
  puts error.object_id.to_s(16)
end

-2444babc
-2444babc

Re-raise with new message

Calls #exception() to create a new message.

begin
  begin
    raise "Error A"
  rescue => error
    puts error.inspect + ": " + 
      error.object_id.to_s(16)
    raise error, "Error B"
  end
rescue => error
  puts error.inspect + ": " + 
    error.object_id.to_s(16)
end
#<RuntimeError: Error A>: -2442c37e
#<RuntimeError: Error B>: -2442c3e2

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

Re-raise with message, backtrace

Calls #exception() and #set_backtrace

begin
  begin
    raise "Error A"
  rescue => error
    puts error.backtrace.first
    raise error, "Error B", ["FAKEFILE:42"]
  end
rescue => error
  puts error.backtrace.first
end

-:3
FAKEFILE:42

Re-raise with no arguments

begin
  raise "Error A"
rescue
  raise
end

…is equivalent to…

begin
  raise "Error A"
rescue => error
  raise error
end

Disallowing double-raise

module NoDoubleRaise
  def error_handled!
    $! = nil
  end

  def raise(*args)
    if $! && args.first != $!
      warn "Double raise at #{caller.first}, aborting"
      exit!
    else
      super
    end
  end
end

Using NoDoubleRaise

class Object
  include NoDoubleRaise
end

begin
  raise "First error"
rescue
  raise "Second error"          # not allowed
end

begin
  raise "First error"
rescue
  error_handled!
  raise "Second error"          # OK!
end

Uncaught exceptions

trap("EXIT") { puts "trap" }
at_exit { puts "at_exit" }
END { puts "END" }

raise "Not handled"
trap
END
at_exit
unhandled.rb:5: Not handled (RuntimeError)

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

Responding to Failures

panic.jpg

Return an error value

Typically nil

def save
  # ...
rescue
  nil
end

Return a benign value

The system might replace the erroneous value with a phony value that it knows to have a benign effect on the rest of the system. – Code Complete

begin
  response = HTTP.get_response(url)
  JSON.parse(response.body)
rescue Net::HTTPError
  {"stock_quote" => "<Unavailable>"}
end

Remote Failure Reporting

  • Log a message
  • Send an email
  • Third-party Services (Hoptoad, RPM, Exceptional)
  • Here be dragons

Avoid the failure cascade

  • Failures have a way of multiplying
  • Circuit Breaker
    Described by Michael Nygard in "Release It"
  • States
    • Closed
    • Open
    • Half-Open

Your Failure Handling Strategy

Error processing is turning out to be one of the thorniest problems of modern computer science, and you can't afford to deal with it haphazardly. – Code Complete

Questions before raising

  • When should I raise?
  • When shouldn't I raise?

Question #1

Is the situation truly unexpected?

Use exceptions only for exceptional situations. […] Exceptions are often overused. Because they distort the flow of control, they can lead to convoluted constructions that are prone to bugs. It is hardly exceptional to fail to open a file; generating an exception in this case strikes us as over-engineering. – The Practice of Programming

  • Invalid user input isn't unusual
  • ActiveRecord's #save

Use throw for expected cases

Sinatra example:

get '/foo' do
  last_modified some_timestamp
  # ...expensive GET logic...
end

Implementation (simplified):

def last_modified(time)
  response['Last-Modified'] = time
  request.env['HTTP_IF_MODIFIED_SINCE'] > time
    throw :halt, response
  end
end

Question #2

Am I prepared to end the program?

@ug = UserGreeting.find_by_name!("winter_holidays")

Vs.

@ug = UserGreeting.find_by_name("winter_holidays")
unless @ug
  logger.error "Someone forgot to run db:populate"
  @ua = OpenStruct.new(:welcome => "Hello")
end

Question #3

Can I punt the decision?

What constitutes an exception?

Detect errors at a low level, handle them at a high level. […] In most cases, the caller should determine how to handle an error, not the callee. – The Practice of Programming

  • Is an EOF a failurer? Is a missing hash key an failure?
  • How about and HTTP 404?
  • Answer: it depends!
  • By raising an exception you force the issue
  • When in doubt, punt

Caller-defined fallback strategy

h.fetch(:optional_key){ DEFAULT_VALUE }
h.fetch(:required_key) {
  raise "Required key not found!"
}

arr.detect(lambda{"None found"}) {|x| ... }

Caller-defined fallback strategy

fetch_quotes do |resp|
    # do this when the quotes can't be fetched
    raise "Failure fetching quotes: #{resp.code}"
end

Question #4

Am I throwing away valuable diagnostics?

result = some_fifteen_minute_operation()
if result.has_raisins_in_it?
  raise "I HATE RAISINS"
end

Vs.

result = some_fifteen_minute_operation()
if result.has_raisins_in_it?
  problem_flags << :icky_raisins
end
# ...

Question #5

Would continuing result in a less informative exception?

response_code = might_return_nil()
message = codes_to_messages[response_code]
response = "System Status: " + message
# What do you mean "Can't convert nil into string"?!

Vs.

response_code = might_return_nil() or
  fail "No response code"
# ...

Isolate exception handling code

as exception represents an immediate, nonlocal transfer of control - it's a kind of cascading goto. Programs that use exceptions as part of their normal processing suffer from all the readability and maintainability problems of classic spaghetti code. – The Pragmatic Programmer

begin is a code smell

begin
  try_something
  rescue
    begin
      try_something_else
    rescue
      # handle failure
    end
  end
end

Method-level exception clauses

def foo
  # mainline logic goes here
rescue # --- business above, failure below ---
  # error handling goes here
ensure # --- housekeepiong below this line ---
  # cleanup
end

Contingency methods

begin
  something_that_might_fail
rescue IOError
  # handle IOError
end

…becomes…

def with_io_error_handling
   yield
rescue
  # handle IOError
end

with_io_error_handling { something_that_might_fail }

When you least expect it    Incremental ShowFirst

  • What parts of this code can raise exceptions?
    size   = File.size('/home/avdi/.emacs')
    kbytes = size / 1024
    puts "File size is #{size}k"
    
  • Any of it!
    • NoMemoryError
    • SignalException
    • Etc.

Exception Safety

Should a library attempt a recovery when something goes wrong? Not usually, but it might do a service by making sure it leaves information in as clean and harmless a state as possible. – The Practice of Programming

A method's exception safety describes how it will behave in the presence of exceptions.

Critical methods need known exception semantics.

The Three Guarantees

  • The weak guarantee
    The object will be left in a consistent state.
  • The strong guarantee
    The object will be rolled back to its beginning state.
  • The nothrow guarantee
    No exceptions will be raised.

Be specific when eating exceptions

Please don't do this:

begin
  # ...
rescue Exception
end

If you can't match on class, match on message:

begin
  # ...
rescue => error
  raise unless error.message =~ /foo bar/
end

Tag your library exceptions

A routine should present a consistent abstraction in its interface, and so should a class. The exceptions thrown are part of the routine interface, just like the specific data types are. – Steve McConnell

module MyLib::Error; end
rescue Exception => e
  e.extend(MyLib::Error)
  raise e
end
# Client code:
rescue MyLib::Error => e
  # ...

What exception classes do I need?

  • Based on module?
  • Based on software layer?
  • Based on severity?
  • Let's try working backwards…

User Error

"That is not a valid selection. Please check your entry and try again." 403.jpg

Logic Error

"Something has gone wrong. We have been informed and are working on it."

kirk.png

Transient Error

"That service is temporarily unavailable. Please try again later." failwhale.gif

Three essential exception classes

  • UserError
  • LogicError
    • InternalError
    • ClientError (for libraries)
  • TransientError
    • TimeoutError

A Recap

…is not going to happen.

Thank You!

exceptional-ruby-cover-small.png

  • Book!
    • exceptionalruby.com
    • Code: RUBYCONF2011
  • Contact!
    avdi.org / @avdi