Exceptional Ruby

Avdi Grimm (@avdi / avdi.org)

Happy Birthday Josh!

josh.jpg

About this Talk

By the end of this talk you should:

  • Understand the mechanics of Ruby failure handling
  • Be able to architect a robust failure handling strategy
    For apps or libraries.
  • Be exceptionally bored

Definitions

  • This talk is about failure
  • Exceptions are how we signal failures
  • Failures are often caused by errors
  • So what is a failure?

Every method has a contract

  • Bertrand Meyer, Design by Contract
  • Ruby's exception system influenced by Eiffel
  • Preconditions and postconditions
  • Either implicit or explicit

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

In some cases it may not be acceptable to know that a failure will lead to termination and a message. You may want to take control. – Object Oriented Software Construction

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

def assert(value)
  raise(RuntimeError, "Doh!", caller) unless value
end
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

module Kernel
  include RaiseExit
end

  • Note: No override in C extensions

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)
    3. Permit by type (don't ask about future errors of this type)
    4. Permit by line (don't ask about future errors raised from this point)
    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. Call #exception() to get the exception.
  2. Call #set_backtrace
  3. Set $!
  4. Throw exception up the call stack

Step 1: #exception()

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_backtrace

Unless being re-raised with no extra arguments

Step 3: Set $!

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: Throw it up 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

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=nil);
    super(msg);
    @original = original;
  end
end
# ...
rescue => error
  raise MyError.new("Error B", error)
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

-244072b8
-244072b8

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>: -243a6b7a
#<RuntimeError: Error B>: -243a6bde

Re-raise with message and backtrace

Calls #exception() and #set_backtrace

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

-:3
FAKE:42

Re-raise with no arguments

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

…is equivalent to…

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

Disallowing double-raise

module NoDoubleRaise
  def error_handled!
    $! = nil
  end

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

Using NoDoubleRaise

module Kernel
  include NoDoubleRaise
end

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

else

def foo
  yield
rescue
  puts "Only on error"
else
  puts "Only on success"
ensure
  puts "Always executed"
end

foo{ raise "Error" }
puts "---"
foo{ "No error" }
Only on error
Always executed
---
Only on success
Always executed

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

What about in threads?

t = Thread.new { raise "oops" }
begin
  t.join
rescue => error
  puts "Child raised error: #{error.inspect}"
end
Child raised error: #<RuntimeError: oops>

Are exceptions slow?

  • Yes.

    With Ruby 1.8.7, the algorithm… is about 37 times slower when Exceptions are involved. – Simon Carletti

  • Ruby 1.9.1 has similar performance

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

The humble puts

puts "Uh oh, something bad happened"
$stderr.puts "Waiting..."
sleep 1
puts "Done"

…produces:

Waiting...
Uh oh, something bad happened
Done
  • Not auto-flushed by default

$stderr.puts

$stderr.puts "Uh oh, something bad happened"
  • But there's a better way…

warn

warn "Uh oh, something bad happened"
  • Auto-flushed
  • Shorter

Warnings as errors

  • We all know warnings can hide real problems…
    module Kernel
      def warn(message)
        raise message
      end
    end
    
    warn "Uh oh"
    
    -:3:in `warn': Uh oh (RuntimeError)
          from -:7
    

Remote Failure Reporting

  • Log a message
  • Send an email
  • 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

exit(1)

Raises SystemExit

warn "Uh oh"
exit 1

…is quivalent to:

warn "Uh oh"
raise SystemExit.new(1)

There's a better way…

abort

abort "Uh oh"
puts "Will never get here."
  • Prints a message
  • Raises SystemExit with a failure status

exit!(1)

warn "Alas, cruel runtime!"
exit! 1
  • Immediately exits
  • Does not pass go. Does not collect $200.
  • No cleanup.

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

Exceptions shouldn't be expected

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

A rule of thumb for raising

We believe that exceptions should rarely be used as part of a program's normal flow; exceptions should be used for unexpected events. …ask yourself, 'Will this code still run if I remove all the exception handlers?" If the answer is "no", then maybe exceptions are being used in nonexceptional circumstances. – The Pragmatic Programmer

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

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 failure strategy

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

arr.detect(lambda{"None found"}) {|x| ... }
def my_awesome_method
  # ...
  if value
    # ...
  else
    yield
  end
end

Questions before raising

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

Question #1

Is the situation truly unexpected?

puts "Confirm (y/n)"
answer = gets.chomp
raise "Huh?" unless [?y, ?n].include?(answer)

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?

def fetch_quotes(options => {})
  on_error = options.fetch(:on_error) { 
    lambda{|r| 
      raise "Failure fetching quotes: #{r.code}"}}

  response = do_fetch(...)
  if !response.success?
    on_error.call(response)
  end
  # ...
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
  # error handling goes here
end

Guard methods

begin
  something_that_might_fail
rescue IOError
  # handle IOError
end

…becomes…

def handle_io_errors
   yield
rescue
  # handle IOError
end

handle_io_errors { something_that_might_fail }

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.

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 testing

  • A test script
  • An assertion or assertions
  • Record
    Execute script, record a set of call-points
  • Playback
    Execute once for every call-point, forcing exception

Code Under Test

def swap_keys(hash, x_key, y_key)
  temp = hash[x_key]
  hash[x_key] = hash[y_key]
  hash[y_key] = temp
end

Record

h = {:a => 42, :b => 23}
tester = ExceptionTester.new{ swap_keys(h, :a, :b) }

exception-testing-recording.png

Playback

tester.assert{
  # Assert the keys are either fully swapped
  # or not swapped at all
  (h == {:a => 42, :b => 23}) ||
  (h == {:a => 23, :b => 42})
}

exception-testing-playback.png

Validity vs. Consistency

An object can be consistent but invalid.

  • Validity
    Are the business rules for the data met?
  • Consistency
    Can the object operate without crashing, or exhibiting undefined behavior?

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

Define your own error base class

  • Especially important in libraries
  • Wrap other 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

    rescue MyLibrary::Error
      raise
    rescue => error
      raise MyLibrary::Error.new(
        "#{error.class}: #{error.message}",
        error)
    end
    

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!