THE BITESITE BLOG
Bangoperators

Careful with Bang Operators in your Rails Models and ActiveRecord Callbacks (Frozen String error)

coding ruby on rails software

Alright here's a weird one that would have been easy to solve if I thought more about how Ruby works (and how it's not like Java:)).

So here was the issue, we were looping through a set of keys on a hash. Now, you should know that the keys on this hash were strings. So we had something like this

my_hash = {"color" => "blue", "size" => "medium" }

Now, on top of that we had a model that we'll call Person that had a "name" attribute and looked something like this:

class Person < ActiveRecord::Base
  before_validation upcase_name

  private
    def upcase_name
      name.upcase!
    end
end

Ok, for those who don't know, the bang version of upcase, upcase!, uppercases a string in place - it modifies the actual string itself.

So here is the setup, we were looping through keys of the hash:

my_hash.keys.each do |name|
  ...
end

Now, first thing to know is that you CAN'T modify a key in a hash. Anytime you try to modify the keys of a hash, you get an error. So something like this would produce an error:

my_hash.keys.each do |name|
  name.upcase!    # produces frozen string error
end

That all makes sense.

What we were surprised to see was that the following code also produces a similar error:

my_hash.keys.each do |name|
  Person.create(name: name)    # produces frozen string error
end

What's going on here? Well it turns out that the "before_validation" callback tries to upcase the name. But unlike languages like Java, the string that is passed in is the exact same object that exists in the "my_hash.keys" array. (I'm purposely avoiding by-value and by-reference wording here, because I feel it can lead to confusion)

So the hash holds a frozen lock on that string, and then when that string gets passed into the Person constructor, and the callback tries to modify it - you get the frozen string error.

How do you fix it? Change your callback:

class Person < ActiveRecord::Base
  before_validation upcase_name

  private
    def upcase_name
      name = name.upcase
    end
end

This version doesn't modify the original string but rather produces a new string.

Special thanks to d3chapma for helping me figure this one out. Check out his Gist here for more info.

Hope this helps out some peeps!

Caseyli
Casey Li
CEO & Founder, BiteSite