Search code examples
ruby-on-railsrubystringhookbefore-save

Can I use downcase! instead of downcase in a before_save hook to change a value?


I am new to Ruby. I am doing Michael Hartl's Ruby on Rails Tutorial, and the following code is used in the model for User:

before_save { self.email = email.downcase }

In this context, would it be acceptable to instead write:

before_save { self.email.downcase! }

or is this approached flawed for some reason? If it is, can you give me a quick explanation as to why?


Solution

  • TL; DR

    In this context, would it be acceptable to instead write

    before_save { self.email.downcase! }
    

    or is this approached flawed for some reason?

    Don't do that unless the bang method is at the end of a method chain, or unless you know for sure that you don't care about the return value. Bad Things™ can happen otherwise.

    Instead, you should use something that handles corner cases, like one of the following:

    • before_save { self.email.downcase! unless self.email.blank? }
    • before_save { self.email = self.email.to_s.downcase }

    Explanation

    The problem with some bang methods like String#downcase! is that they don't provide the return values you think they do. While self.email.downcase! will downcase the email attribute on self, the return value may be nil. For example:

    "A".downcase!
    #=> "a"
    
    "".downcase!
    #=> nil
    
    "z".downcase!
    #=> nil
    

    Even worse, if email is nil an exception will be raised whether you use downcase or downcase!. For example:

    nil.downcase
    # NoMethodError: undefined method `downcase' for nil:NilClass
    

    For the purposes of simply ensuring that the email attribute is lowercased, then you might get away with this in the narrow circumstance where strong params or other factors ensure that email is not nil, and where you aren't using the return value of your method or hook. More broadly speaking, though, a train wreck like:

    before_save { self.email.downcase!.gsub(?@, ' AT ') }
    

    might blow up in surprising and hard-to-debug ways at runtime.

    To recap, your current examples appear functionally equivalent, but handle return values quite differently. Therefore, your mileage may vary.