Ruby and the forgotten thrown symbol

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

wpDiscuz