String#freeze
str.freeze -> str The freeze method marks an object as immutable. Once frozen, any attempt to modify the object will raise a FrozenError. Strings in Ruby are frozen by default since Ruby 3.0, but you can explicitly freeze mutable objects to prevent accidental modifications. That makes the method useful any time a value should stay stable after setup.
Syntax
str.freeze
The call str.freeze returns the receiver itself after marking it as frozen. It’s a one-liner with no arguments — just call it on the object you want to protect. Once frozen, the receiver cannot be unfrozen; there is no inverse method. If you need a mutable copy later, you have to create a duplicate first.
Parameters
freeze takes no parameters. It operates on the receiver object directly.
Examples
Basic usage
s = "hello"
s.freeze
begin
s << " world"
rescue FrozenError => e
puts e.message
end
# => can't modify frozen String
String literals in Ruby 3.0 and later are frozen by default, which means you rarely need to call freeze on a plain string. The frozen? predicate confirms that the object is already immutable. When you do need a mutable copy, dup creates an unfrozen duplicate that you can modify freely — this is the standard pattern for working with string literals that need to change.
Working with string literals
# In Ruby 3.0+, string literals are automatically frozen
"hello".frozen? # => true
# Create a mutable string with dup
s = "hello".dup
s.frozen? # => false
s << " world" # works fine
s # => "hello world"
Frozen string literals aren’t just about safety; they also help Ruby save memory. Because a frozen string can never change, the interpreter reuses the same object in memory every time that literal appears in source code. The example below demonstrates this by creating a thousand-element array and checking how many distinct objects live behind the scenes.
Freeze for memory efficiency
# Reusing a frozen string literal saves memory
urls = Array.new(1000) { "https://example.com" }
urls.each(&:object_id).uniq.size # => 1 (all point to same frozen string)
# If you need unique strings, they each consume memory
unique_urls = Array.new(1000) { "https://example.com".dup }
unique_urls.each(&:object_id).uniq.size # => 1000
Beyond individual strings, freeze works well on the larger data structures that programs rely on. Configuration hashes, lookup tables, and constant arrays often carry values that should never change after startup. The pattern below wraps a config hash in a constant and freezes it on the same line, so any accidental write raises immediately.
Common patterns
Protecting configuration
CONFIG = {
max_retries: 3,
timeout: 30
}.freeze
CONFIG[:max_retries] = 5 # => FrozenError
It’s worth noting that freeze makes the collection itself immutable but does not recursively freeze the elements inside it. A frozen array of strings still holds unfrozen strings unless you freeze each one individually. For deeply immutable structures, you would need to walk the object and freeze every level explicitly.
Freezing arrays and hashes
# Freeze composite objects to make them immutable
colors = ["red", "green", "blue"].freeze
colors << "yellow" # => FrozenError
settings = { env: "production", debug: false }.freeze
settings[:new_key] = true # => FrozenError
When you’re building symbols from strings, freezing the intermediate strings can help Ruby reuse memory under the hood. The map(&:freeze) call in the pattern below runs before to_sym, giving the runtime a frozen string that it can safely deduplicate before converting to a symbol. This matters most in code paths that process many string-to-symbol conversions, like parsing CSV headers or normalizing hash keys.
Deduplication with freeze
# Force string deduplication
symbols = %w[foo bar baz].map(&:freeze).map(&:to_sym)
# Creates symbols: :foo, :bar, :baz
Errors
| Error | When it occurs |
|---|---|
FrozenError | Attempting to modify a frozen object |
Freezing with purpose
freeze is most useful when an object should stop changing after a certain point in the program. That can protect shared configuration, cached data, or literal values that should not be altered by accident. Freezing makes the intent visible: this object is finished, and later code should treat it as read-only. That can make a codebase easier to reason about because the places that may mutate a value become much easier to spot.
It also helps with sharing and memory use when the same string or object appears many times. Once the value is frozen, Ruby can reuse it more confidently, and the code no longer needs to worry about hidden mutation. For code that values stability, freeze is a simple but very effective signal.
Freezing before sharing
Freezing is often most useful right before a value leaves the method that created it. At that point the object has already been shaped the way the code wants, and freezing tells the rest of the program not to treat it as writable. That can make constants, cached strings, and shared configuration easier to trust. If a later step really needs to change the value, it can duplicate the object first and make the mutation on the copy instead.