Frozen String Literals and Immutability
Ruby strings are mutable by default. You can call << to append, gsub! to replace, or encode to transform them in place. This mutability is convenient, but it comes with a hidden cost: every time you write a string literal, Ruby allocates a new object. If your program creates thousands of identical string literals, that’s thousands of separate objects the garbage collector has to track.
The frozen_string_literal pragma addresses this — and introduces immutability as a side effect. Understanding when and why to use it will make your Ruby code faster and your intent clearer.
What the Pragma Does
Place # frozen_string_literal: true at the very top of a Ruby source file:
# frozen_string_literal: true
class Greeting
def initialize(name)
@name = name
end
def hello
"Hello, #{@name}!"
end
end
From that point in the file, all string literals are frozen — they become immutable String objects. If any code tries to modify one, Ruby raises a FrozenError:
# frozen_string_literal: true
greeting = "hello"
greeting << " world" # FrozenError: can't modify frozen String
Why Frozen Strings Improve Performance
Without the pragma, Ruby allocates a new String object each time it encounters a literal. Even if two literals contain the same text, they’re separate objects with different object IDs:
# without frozen_string_literal
3.times do
puts "hello".object_id
end
# prints three different numbers
With the pragma, Ruby can reuse the same frozen object across the file, since immutable objects are safe to share:
# frozen_string_literal: true
3.times do
puts "hello".object_id
end
# prints the same number three times
Fewer allocations means less work for the garbage collector. In applications that create large numbers of strings — think template-heavy web apps or data processing scripts — this can add up to measurable performance gains.
What Gets Frozen — and What Doesn’t
The pragma freezes only static string literals — the things in quotes. Everything else stays mutable:
# frozen_string_literal: true
# These are frozen:
"hello"
'hello'
"hello #{10}" # interpolating a literal number is still static, thus frozen
# These are NOT frozen:
"hello #{name}" # dynamic — depends on runtime value
string = "hello" # the variable, not the literal
string.upcase # returns a new string (not the original)
In Ruby 3.0+, interpolated strings are always mutable, even with the pragma. Only the non-interpolated parts are frozen.
The FrozenError Exception
When Ruby 2.x encountered an attempt to modify a frozen string, it raised a plain RuntimeError. Starting with Ruby 3.0, this is a dedicated FrozenError:
# frozen_string_literal: true
s = "hello"
s << " world"
# FrozenError (Ruby 3.0+)
# RuntimeError (Ruby 2.x)
If you need to handle frozen string mutations, rescue FrozenError:
begin
s << " world"
rescue FrozenError
s = s + " world" # create a new mutable string instead
end
Mutating a Frozen String
When you genuinely need a mutable version, String#dup or String#clone creates a copy:
# frozen_string_literal: true
original = "hello"
copy = original.dup # copy is mutable
copy << " world"
puts copy # "hello world"
puts original # "hello" — unchanged
Freezing Objects Explicitly with freeze
The pragma only covers string literals in the file where it appears. For any other object — or for strings you want to freeze at runtime — call freeze directly:
config = {
api_key: ENV["API_KEY"],
endpoint: "https://api.example.com",
}.freeze
config[:api_key] = "new" # FrozenError
freeze works on any object, not just strings:
array = [1, 2, 3].freeze
array << 4 # FrozenError
freeze is shallow — it freezes the object itself but not any objects it contains:
frozen = [[1, 2], [3, 4]].freeze
frozen[0] << 99 # no error — inner arrays are not frozen
frozen << [5, 6] # FrozenError — the array itself is frozen
For deep freezing, you need to recursively freeze nested structures or use a gem like deep_freeze.
Enabling Globally
Instead of adding the pragma to every file, you can enable it project-wide:
ruby --enable-frozen-string-literal your_script.rb
Many Ruby’s performance guides recommend enabling it globally in production, since the memory and GC benefits apply to all strings throughout the application.
Constants and Immutability
A related concern is constant immutability. By default, constants holding mutable objects can be modified:
MAX_USERS = [100, 200, 500] # array is mutable
MAX_USERS << 1000 # no error
You can use the shareable_constant_value pragma to automatically freeze constant values:
# shareable_constant_value: literal
MAX_USERS = [100, 200, 500] # array is deeply frozen
MAX_USERS << 1000 # FrozenError
When to Use the Pragma
Use # frozen_string_literal: true in:
- All new Ruby files you write — it’s a good default habit
- Performance-critical code with many string literals
- Code you want to publish as gems (it was introduced to help gems prepare for Ruby 3.0)
The pragma has no real downside in most cases — if you’re not modifying string literals, making them frozen costs you nothing. The GC and allocation benefits are free immutability.
See Also
- ruby-working-with-strings — full coverage of string manipulation in Ruby
- ruby-blocks-procs-lambdas — closures and how they capture variable bindings
- ruby-string-manipulation — string transformation techniques