DEV Community

Syed Aslam
Syed Aslam

Posted on • Originally published at syedaslam.com on

When ||= Betrays Your Memoization

Memoization in Ruby often looks harmless:

def active?
  @active ||= compute_active_flag
end
Enter fullscreen mode Exit fullscreen mode

It’s clean. It’s idiomatic. It avoids duplicate work.

For years, this pattern feels safe.

But ||= does not mean “if unset.”
It means “if falsey.”

And in long-lived systems, that difference matters.

The subtle behavior

This:

@active ||= compute_active_flag
Enter fullscreen mode Exit fullscreen mode

means Ruby assigns only when @active is nil or false.

So, since nil and false are the only falsey values in Ruby:

  • @active is nil -> compute
  • @active is false -> compute
  • @active is truthy -> reuse

Memoization with ||= is only reliable when the memoized value is guaranteed to become truthy.

How this causes confusion over time

In one system I worked on, a boolean flag was memoized exactly like this.

At first, the flag was effectively “unset or true,” and everything worked.

Later, the domain evolved. false became an explicit, meaningful result based on new business rules.

That’s when it got strange:

  • the method recomputed unexpectedly, silently destroying performance if compute_active_flag was expensive
  • tests became noisy and inconsistent
  • code readers assumed memoization was happening, because the syntax looked memoized

The code looked correct.
The semantics weren’t.

That gap is where long-lived systems accumulate friction.

Why this happens

As systems mature:

  • flags gain new states
  • defaults become explicit
  • “maybe missing” becomes “intentionally false” (or sometimes intentionally nil)

But ||= never changes its behavior. It keeps encoding a truthiness assumption that may no longer fit your domain.

This isn’t a Ruby flaw.
It’s a reminder that convenience operators carry semantics.

Safer patterns

If false is valid, but nil still means “unset”:

def active?
  return @active unless @active.nil?
  @active = compute_active_flag
end
Enter fullscreen mode Exit fullscreen mode

If both false and nil are legitimate cached values, guard on definition instead using defined? (which is slightly faster, as it's evaluated by the parser rather than as a method call) or instance_variable_defined?:

def active?
  return @active if defined?(@active)
  @active = compute_active_flag
end
Enter fullscreen mode Exit fullscreen mode

Now false and nil are preserved as cached results, and behavior matches intent.

It’s a few more characters.
It’s also semantically honest.

The broader lesson

Most issues in long-running systems aren’t dramatic failures.

They’re small mismatches between yesterday’s assumptions and today’s domain.

||= is a perfect example: convenient, idiomatic, and excellent in the right context.

But when false or nil becomes meaningful, tiny semantics compound.

Explicitness costs a little now.
It usually saves much more later.


References

Top comments (0)