Calling += on a frozen String
Question
What is the return value of the following Ruby code?
str = "abc".freeze
str += "def"
The correct answer is
-
It raises
FrozenError -
"abcdef"Correct -
"abc" -
nil
Explanation
TL;DR
str += "def" is syntactic sugar for str = str + "def". String#+ never mutates its receiver; it returns a new String, and the assignment rebinds the local variable str to that new object. The frozen "abc" is never modified, so no FrozenError is raised and the expression returns "abcdef".
Step by step
str = "abc".freeze
original = str
str += "def" # => "abcdef"
str # => "abcdef"
original # => "abc"
str.equal?(original) # => false
str.frozen? # => false
str + "def" allocates a fresh String containing "abcdef", then the assignment points str at it. The original frozen "abc" still exists, untouched, here kept reachable through original. Note that the result is not frozen: String#+ returns a plain new String regardless of whether its operands are frozen.
Under the hood
There is no += method in Ruby. The parser rewrites a += b into a = a + b before compilation, so the only method call involved is String#+, and the rest is variable assignment. freeze forbids mutating an object; it says nothing about the variables that reference it. Variables are just names in the local table and can always be rebound. Objects are frozen, variables are not.
Edge cases
Operations that mutate the receiver in place do raise:
str = "abc".freeze
begin
str << "def"
rescue FrozenError => e
e.message # => "can't modify frozen String: \"abc\""
end
The same goes for concat, upcase!, gsub!, and every other in-place mutator: they all raise FrozenError on a frozen string. The rule of thumb: += builds a new object, << edits the existing one.
The # frozen_string_literal: true magic comment freezes every string literal in the file, and += still works there for the same reason:
# frozen_string_literal: true
str = "abc"
str.frozen? # => true
str += "def" # => "abcdef"
str.frozen? # => false
Version notes
FrozenError was added in Ruby 2.5 as a subclass of RuntimeError; before that, mutating a frozen object raised a plain RuntimeError with the same message. Ruby 3.4 introduced chilled string literals: in files without a frozen_string_literal comment, mutating a string literal emits a deprecation warning (when deprecation warnings are enabled) as a step toward frozen-by-default literals.
Share this quiz
Comments
No comments yet. Be the first.