As usual Rails 5 brought many changes. Among these, there’s the new callbacks behaviour for ActiveRecord that uncovered an old and forgotten ruby construct: throw
.
In case you don’t know what I’m talking about, let’s say I don’t want captains to be removed from my app, but plain users only. In Rails 4 I can write the following:
class User < ActiveRecord::Base
scope :plain, -> { where captain: false }
scope :captain, -> { where captain: true }
before_destroy :ensure_not_captain
private
def ensure_not_captain
return true unless captain?
errors.add :base, 'Captains cannot be destroyed'
return false
end
end
This results in the following:
plain_user = User.plain.first
plain_user.destroy # user is destroyed
captain = User.captain.first
captain.destroy # => false
captain.errors.full_messages.first # => 'Captains cannot be destroyed'
But Rails 5 changed this, and now the above code wouldn’t work because returning false
inside a callback will not halt the flow anymore. Instead you have to do the following:
def ensure_not_captain
return unless captain?
errors.add :base, 'Captains cannot be destroyed'
throw :abort
end
Okay, what’s that throw? At the very moment I looked at this code I thought it could be a synonym of raise
. Why not? Our raise
is after all a throw
in Java.
But in that code I read throw :abort
. I’m sending a symbol to throw
, while everyone knows that raise
accepts only strings, exception classes or exception instances:
raise Exception # works
raise Exception.new # works
raise 'asd' # works
raise :asd # raises a TypeError
raise Object # raises a TypeError
raise Object.new # raises a TypeError
I had to go back to my first ruby studies to find this throw
keyword I had completely forgotten about, because since I switched to Ruby (it was the end of the 2010) I never ever used or read (not a single time) the throw keyword. Never.
Again, what’s that throw
then? Differently from the raise
keyword, the throw
is not used for errors but for control flow. It’s used in pair with catch
and its usage is similar to raise
:
songs = []
catch :done do
while typed = gets.strip
throw :done if typed == "!"
songs << typed
end
end
puts "You typed the following songs: #{songs}"
The above code will continue asking for song names until I type a “!”. In that case it will throw :done
existing from the catch
block. Let’s see a more complex example:
def generate_random_numbers
catch :random_numbers do
result = []
10.times do
number = rand 100
throw :random_numbers, result if number < 10
result << number
end
result
end
end
generate_random_numbers # => [10, 27]
generate_random_numbers # => [55, 75]
generate_random_numbers # => [28, 53, 40, 13, 76, 22, 41, 20, 15, 44]
This block will generate up to 10 random numbers. But it will exit with the current ones as soon as it gets a number below 10. This can be done because by default a catch
block will return the last statement but you can ovverride it specifying the second argument in throw
.
And last but not least, in case you are thinking you could anyway live without throw
and using the raise
also for control flow, there’s the performance thing. Exceptions are slow, really really slow, because raising an exception forces ruby to build and dump the stack trace. The throw/catch
is instead blazingly fast because no stack trace needs to be built.
So, thank you Rails for having unconvered the throw/catch
construct. I had completely forgotten about it, and I promise I’ll try to use it more because I think it can improve my code.
Leave a Reply