Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created October 8, 2018 15:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/960a9a39fec43c164e322c6310eb5718 to your computer and use it in GitHub Desktop.
Save JoshCheek/960a9a39fec43c164e322c6310eb5718 to your computer and use it in GitHub Desktop.
try/throw vs raise/rescue in Ruby
RUBY_ENGINE # => "ruby"
RUBY_VERSION # => "2.5.1"
RUBY_PLATFORM # => "x86_64-darwin17"
# QUESTION
#
# Say you want to bail from a web controller as soon as some method is called
# Eg in Rails, maybe you want a `render!` method, which is the same as render,
# but it also returns from the method that called it. You could accomplish this
# with either raise/rescue or throw/catch. This is the sort of thing throw/catch
# were meant to handle, but most times that situations like this (control flow)
# occur, people use exceptions instead (throw/catch is a bit of a forgotten
# features). So, is there any performance cost to choosing the one over the other?
#
#
# CONCLUSION
#
# * throw / catch is fastest
# * raise / rescue is 6.2 times slower if you raise a new error each time
# * raise / rescue is 1.5x slower if you allocate 1 error and always throw that one instance
# * there is no relevant difference between rescuing the class vs a superclass
# * raise / rescue is 40x slower if you access the backtrace (before this was
# optimized, its overhead + the fact that Twitter was raising an exception
# every time they would iterate over an array, were what led to the fail whale
# and their whining about Rails scaling) Imagine switching languages because
# you think a language is too slow because every time you call .each, you wind
# up incurring this overhead. source: it's somewhere in this hilarious talk
# https://www.youtube.com/watch?v=LjFM8vw3pbU
class Status < StandardError
attr_reader :code
def initialize(code)
@code = code
super "Status #@code"
end
end
class Unauthorized < Status
def initialize
super 401
end
end
# raise / rescue, don't allocate the error...
# there could be other issues with doing this, eg it's probably not threadsafe
# I don't think I've ever seen this in practice
@err = Unauthorized.new
def will_raise1_reuse() will_raise2_reuse end
def will_raise2_reuse() raise @err end
will_raise1_reuse rescue $!.code # => 401
# raise / rescue, make a new error each time (much much more common)
def will_raise1_allocate() will_raise2_allocate end
def will_raise2_allocate() raise Unauthorized end
will_raise1_allocate rescue $!.code # => 401
# throw / catch
def will_throw1() will_throw2 end
def will_throw2() throw :status, 401 end
catch(:status) { will_throw1 } # => 401
# kk, lets benchmark them using https://github.com/evanphx/benchmark-ips
require 'benchmark/ips'
Benchmark.ips do |x|
x.report "raise, reusing the same error, rescue the class" do
begin will_raise1_reuse
rescue Unauthorized # Unauthorized implies 401, so we don't need access to the error to know its arg
end
end
x.report "raise, reusing the same error, rescue a superclass" do
begin will_raise1_reuse
rescue Status => err # rescuing `Status` vs `Object` had no discernable difference
err.code # `Status` is general, so we need access to the error to get its code
end
end
x.report "raise, allocating a new error, rescue the class" do
begin will_raise1_allocate
rescue Unauthorized
end
end
x.report "raise, allocating a new error, rescue the class, access the backtrace" do
begin will_raise1_allocate
rescue Unauthorized => err
err.backtrace # Ruby has an optimization where it won't build
end
end
x.report "throw / catch" do
catch(:status) { will_throw1 }
end
x.compare!
end
# >> Warming up --------------------------------------
# >> raise, reusing the same error, rescue the class
# >> 80.002k i/100ms
# >> raise, reusing the same error, rescue a superclass
# >> 76.463k i/100ms
# >> raise, allocating a new error, rescue the class
# >> 25.144k i/100ms
# >> raise, allocating a new error, rescue the class, access the backtrace
# >> 4.392k i/100ms
# >> throw / catch 103.448k i/100ms
# >> Calculating -------------------------------------
# >> raise, reusing the same error, rescue the class
# >> 1.277M (± 8.0%) i/s - 6.400M in 5.044363s
# >> raise, reusing the same error, rescue a superclass
# >> 1.167M (± 9.6%) i/s - 5.811M in 5.030423s
# >> raise, allocating a new error, rescue the class
# >> 297.901k (± 6.4%) i/s - 1.483M in 5.001602s
# >> raise, allocating a new error, rescue the class, access the backtrace
# >> 46.733k (± 7.0%) i/s - 232.776k in 5.003789s
# >> throw / catch 1.838M (± 8.8%) i/s - 9.207M in 5.050553s
# >>
# >> Comparison:
# >> throw / catch: 1837975.2 i/s
# >> raise, reusing the same error, rescue the class: 1277467.8 i/s - 1.44x slower
# >> raise, reusing the same error, rescue a superclass: 1166865.2 i/s - 1.58x slower
# >> raise, allocating a new error, rescue the class: 297900.6 i/s - 6.17x slower
# >> raise, allocating a new error, rescue the class, access the backtrace: 46733.2 i/s - 39.33x slower
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment