1. Exceptions in Ruby
Tips & Tricks
Vlad ZLOTEANU
#ParisRB
July 3, 2002 Software Engineer @ Dimelo
@vladzloteanu
Copyright Dimelo SA www.dimelo.com
2. About me
R&D Software Engineer @ Dimelo
@vladzloteanu
https://github.com/vladzloteanu
Copyright Dimelo SA www.dimelo.com
3. Exceptions (in Ruby)
Failure: abnormal situation
Exception: instance of Exception class (or instance of
descendant)
What: allows packaging info about the failure
message string
backtrace
default behavior: program terminates (exits)
Copyright Dimelo SA www.dimelo.com
5. Raising an exception
class MyException < StandardError; end
raise MyException.new
raise # aka: raise RuntimeError.new
raise “Boom!!” # aka raise RuntimeError.new(“Boom!!”)
raise MyException, “Boom!!”, backtrace
Copyright Dimelo SA www.dimelo.com
6. Rescuing exceptions
begin
raise MyError.new("Booooom!")
rescue MyError
puts "MyError just hapened"
rescue # Will rescue StandardError
puts "a RuntimeError just happened"
rescue Exception => e
puts ”Exception: #{e.message}”
e.backtrace.each{|line| puts “ > #{line}”}
end
Copyright Dimelo SA www.dimelo.com
7. Intermezzo: Signals
Signals – used to alert threads/processes
(hardware faults, timer expiration, terminal
activity, kill command, etc)
SIGINT: CTRL+C in terminal (can be caught)
SIGTERM: Sent by kill command by default
(can be caught)
SIGKILL: Uncatchable exception (kill -9)
A SignalException is raised when a signal is
received
Copyright Dimelo SA www.dimelo.com
8. What should we rescue?
Copyright Dimelo SA www.dimelo.com
9. Rescuing exceptions
TIP: DON’T RESCUE ‘EXCEPTION’ class!
begin
while(true) ; end
rescue Exception
puts "Rescued exception; Will retry"
retry
end
^CRescued exception; WIll retry
^CRescued exception; WIll retry
^CRescued exception; WIll retry
# Kill -9, baby!
Copyright Dimelo SA www.dimelo.com
10. Rescuing exceptions (third-party libraries)
# You may think this will save you from all evil ..
begin
Net::HTTP.get_response(’external.resource', '/')
rescue => e
[…]
# Think again!
# Excerpt from net/http.rb
def connect
s = timeout(@open_timeout)
{ TCPSocket.open(conn_address(), conn_port()) }
Net::HTTP(Timeout library) may throw
Timeout::Error < InterruptError
Copyright Dimelo SA www.dimelo.com
11. Rescuing exceptions (third-party libraries)
# Solution is easy:
begin
Net::HTTP.get_response(’external.resource', '/')
rescue StandardError, Timeout::Error => e
[…]
Tip: Net::HTTP (and many of the gems that
depend on it) can throw Timeout::Error – don’t
forget to rescue it
Copyright Dimelo SA www.dimelo.com
12. Rescuing exceptions - cont
TIP: (rescue blocks): rescue most specific
errors first, most generic last
TIP: inline rescue
but avoid it because:
No access to rescued exception
Exceptions are slow (explained later)
# Will return: DefaultDecorator
@decorator = “foo”.constantize rescue DefaultDecorator
Copyright Dimelo SA www.dimelo.com
13. What should we raise?
Copyright Dimelo SA www.dimelo.com
14. Raising exceptions
Tip: make your own exception class, do not
“raise string”
avoid using error message for making
discriminating error type
begin
if @avatar_url.empty?
raise “Input should not be empty”
elsif @avatar_url.valid?
raise “Avatar URL invalid”
end
rescue RuntimeError => e
# They are both instances of RuntimeError, can’t
# rescue only the first one
end
Copyright Dimelo SA www.dimelo.com
15. Raising exceptions (cont)
# excerpt from OpenSSL::SSLError
# - invalid certificate, invalid hostname, protocol
mismatch, etc.
class SSLError
def initialize(message)
@message = message
end
[…]
# Client code
rescue OpenSSL::SSL::SSLError => e
if e.message =~ /sslv3 alert unexpected message/
@conn.ssl_version="SSLv3“
retry
else
raise
Copyright Dimelo SA www.dimelo.com
17. Intermezzo: caller method
TIP: ’caller’ gets you current execution
stacktrace
def foo
bar
end
def bar
puts caller.inspect
end
foo
# [”test_caller:2:in `foo'",
”test_caller.rb:9"]
Copyright Dimelo SA www.dimelo.com
18. Re-raise exception – backtrace issue
def foo
Net::HTTP.get_response('does.not.exists', '/')
rescue => e
# some logging, or whatever..
# raising a new error
raise MyCustomException.new(‘Boom!’)
end
begin
foo
rescue => e
puts e.backtrace.inspect
end
["reraise_ex.rb:7:in `foo'", "reraise_ex.rb:11"]
Copyright Dimelo SA www.dimelo.com
19. Re-raise exception – backtrace issue (2)
TIP: when reraising new exception, ensure you
don’t loose the backtrace
def foo
Net::HTTP.get_response('does.not.exists', '/')
rescue => e
# raising a new error, keeping old backtrace
raise MyException, "And now.. boom!", e.backtrace
# another option is to reraise same error:
raise
End
Copyright Dimelo SA www.dimelo.com
20. Ensure
begin
puts '> always executed (even if there was
normal flow’ or there
raise "error in normal flow”
puts "< was not flow"
normal an exception)
rescue
good place for code cleanup
puts "> RESCUE - last error is <#{$!}>”
raise "error in rescue”
puts "< RESCUE"
ensure
puts "> ENSURE - last error is <#{$!}>”
raise "error in ensure”
puts "< ENSURE after raising"
end
ruby raise_rescue.rb
> normal flow
> RESCUE - last error is <error in normal flow>
> ENSURE - last error is <error in rescue>
www.dimelo.com
raise_rescue.rb:11: error in ensure (RuntimeError)
Copyright Dimelo SA
21. Catch/throw
ruby ‘goto label’ implementation
should be used on ‘expected’ situations
# Sinatra code
def last_modified(time)
response['Last-Modified'] = time
request.env['HTTP_IF_MODIFIED_SINCE'] > time
throw :halt, response
end
end
def invoke
res = catch(:halt) { yield }
..
end
Copyright Dimelo SA www.dimelo.com
22. Exceptions are slow ?
https://github.com/vladzloteanu/ruby_exceptions_benchmark
user_name = ““
~ 0.3ns
# Test with ‘if’
nil if user_name.empty?
# Test with raise/rescue
begin ~ 74 ns
raise "boom!" if user_name.empty?
rescue => e
nil
end
~ 2 ns
# Test with try/catch
catch(:error) do
throw :error if user_name.empty?
end
Copyright Dimelo SA www.dimelo.com
23. Exceptions are slow ? (2)
class User < ActiveRecord::Base
validates :email, :presence => true
end
user = User.new
# Test with ‘if’
~ 1.5 ms
nil unless user.save
# Test with raise/rescue
~ 2 ms
user.save! rescue nil
Copyright Dimelo SA www.dimelo.com
24. Exceptions are slow ? (3)
TIP: Exceptions should be used only on
‘exceptional’ situations
Use conditional constructs or ‘throw’ for
expected cases
TIP: Detect errors at low level, handle them at
high level
Copyright Dimelo SA www.dimelo.com
25. Recap
Don’t rescue Exception!
Watch out for exceptions thrown by your libs
(Timeout::Error on Net::HTTP) Avoid putting
error type (only) in exception message
Avoid using exceptions for normal program
flow
Exceptions are slow(er)
Catch errors on the caller code, not on the
callee
When you reraise, ensure that you don’t
loose initial error’s backtrace
Copyright Dimelo SA www.dimelo.com
26. Thank you!
Questions?
http://jobs.dimelo.com ;)
Copyright Dimelo SA www.dimelo.com