ActiveSupport’s #try might not be doing what you think it’s doing

Today I had reason to verify the exact semantics of ActiveSupport’s Object#try extension for an upcoming RubyTapas episode. #try is usually used to paper over nil values. Unfortunately, #try does more than this, and as a result it can easily hide defects.

Consider: we have a variable with a number in it. We want to round the number down.

num = 23.1
num.floor # => 23

Something that sometimes happens in our applications is that we take in some numeric information in string form, and forget to convert it to a number before using it as one. This is a bug that we need to be aware of, and Ruby is more than happy to let us know with an exception.

num = "23.1"
num.floor # =>

# ~> NoMethodError
# ~> undefined method `floor' for "23.1":String
# ~>
# ~> xmptmp-in49044q82.rb:2:in `'

Now let’s say we know that the value of num is sometimes nil . Rather than removing this nil incursion from our code, we decide to turn the nil case into a harmless no-op with .try .

num = nil
num.try(:floor) # => nil

Remember the previous example, where we got a string and forgot to turn it into a number, and Ruby told us all about it? Guess what happens now:

num = "23.1"
num.try(:floor) # => nil

Congratulations: we now have a silent defect. Good luck finding it in the absence of a NoMethodError .

Why? Because #try doesn’t actually care about nils. It’s only concerned with whether an object responds to the given message. This is consistent with the method’s name. In my experience, though, it is not consistent with what most programmers actually want when they use #try .

Incidentally, Ruby’s recently-added “safe navigation” operator does not share this drawback. It is strictly nil-sensitive.

(I’ll be covering this issue and a whole array of related techniques in an upcoming RubyTapas miniseries.)

UPDATE: A few people have pointed out that at least as of Rails 4.0, there is now a #try! variant with the safer semantics. At the time of writing, this version has yet to make an appearance in the official ActiveSupport Rails Guide.

If you’re using Rails 4 or later, this new version should probably be your default choice.

6 comments

  1. The best part is that try had an original semantic, changed, and then changed back.

    Original implementation:

    https://github.com/rails/rails/commit/51730792ca930a896361eb92354a42bc56903de1

    "23.1".try(:floor) # => nil

    Rails 2.3 through 3.x:

    https://github.com/rails/rails/blob/2-3-stable/activesupport/lib/active_support/core_ext/try.rb#L25-L36

    "23.1".try(:floor) # => NoMethodError

    Rails 4.0:

    https://github.com/rails/rails/blob/v4.0.0/activesupport/lib/active_support/core_ext/object/try.rb#L45

    "23.1".try(:floor) # => nil

Leave a Reply

Your email address will not be published. Required fields are marked *