Common Hash issue

Did you get RuntimeError: can't add a new key into hash during iteration error?

In this post we will check together a common pattern and how to solve it easily.

The error message is self descriptive, we can’t update a hash during iteration and it can be reproduced with the following snippet:

hash = {title: "basch the hash", pages: 404}
hash.each do |k,v|
  hash["#{k}-dup"] = v
end

So here is a quick question, what will be the output of entry variable after executing track(entry)

entry = {title: "Funny memes", pages: 404}

def track(hash)
  hash[:title] = "#{hash[:title].upcase}-TITLE-DATA}"
  # send hash to a tracking service
  fake_service.send(hash)
end

track(entry)
# output of entry var:
p entry

Surprise, the original hash was modified, and we can confirm that we are using the same object by printing the object id.

entry = {title: "Funny memes", pages: 404}
p "entry object id: #{entry.object_id}" # entry object id: 32144

def track(hash)
  p "hash object id: #{hash.object_id}" # hash object id: 32144
  hash["title"] = "#{hash[:title].upcase}-TITLE-DATA}"
  # send hash to a tracking service
  fake_service.send(hash)
end

So what are our options in this case?

We can simply use a copy of the hash instead of modifying the original one. If this snippet is used in a Rails project we can take advantage of ActiveSupport library by using deep_dup method.

And if I’m not using ActiveSupport? No worries there is another option also which is Marshalling:

entry = {title: "Funny memes", pages: 404}
p "entry object id: #{entry.object_id}" # entry object id: 32144

entry_copy = Marshal.load(Marshal.dump(entry))
p "entry_copy object id: #{entry.object_id}" # entry object id: 56333

It’s better to wrap in a utility like this:

module Utility
   # Avoid using deep_dup name here, it will be confusing trust me ;)
   def self.deep_duplicate(collection)
     Marshal.load(Marshal.dump(collection))
   end
end

A quick note, as you will see in the example below, I’m creating a duplicate just before updating the hash. The reason for this is to make it easier for us to understand that we are updating a copy and not the original hash.

Hmmm and what if I pass a copy to the method? In theory, it’s a good suggestion but keep in mind that your application will be harder to understand as you will be always be asking: “Am I passing a copy or the original Hash here? :D”

entry = {title: "Funny memes", pages: 404}

def track(hash)
  # hash_copy = Utility.deep_duplicate(hash)
  hash_copy = hash.deep_dup
  hash_copy["title"] = "#{hash[:title].upcase}-TITLE-DATA}"
  # send hash to a tracking service
  fake_service.send(hash_copy)
end

That’s it, so have fun and keep coding 🙂