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
raise
(or fail
)
- Every exception starts with a
raise
(orfail
)
- 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"
- Get the excepion object.
- Set the backtrace.
- Set the global error info variable.
- 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 callingException.new()
.
Exception#exception
- With no arguments, returns self.
- With a message, returns a new exception.
- With no arguments, returns self.
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
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
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
- Closed
- Ruby implementation
https://github.com/wsargent/circuit_breaker
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.
- NoMemoryError
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."
Logic Error
"Something has gone wrong. We have been informed and are working on it."
Transient Error
"That service is temporarily unavailable. Please try again later."
Three essential exception classes
- UserError
- LogicError
- InternalError
- ClientError (for libraries)
- InternalError
- TransientError
- TimeoutError
- TimeoutError
A Recap
…is not going to happen.
Thank You!
- Book!
- exceptionalruby.com
-
Code:
RUBYCONF2011
- Rate!
http://spkr8.com/t/8526
- Notes / Links
http://avdi.org/devblog/exceptional-ruby/
- Contact!
avdi.org / @avdi
- Photo Credits
"Panic Button" by Michael Manitius