rubyguides

The Null Object Pattern in Ruby

What is the null object pattern?

The Null Object pattern replaces nil references with objects that implement the same interface as real objects but do nothing. Instead of checking if user.nil? everywhere, you inject a null version of User that responds to the same methods but returns safe, harmless values.

Instead of branching on absence, your code works with a well-defined nothing object. The calling code never needs to branch on nil, which makes the API easier to read and the control flow easier to follow.

The payoff is real:

  • Conditionals disappear from calling code
  • New edge cases cannot sneak in, because you do not forget a nil check in a random branch
  • The “nothing” behaviour lives in one place, not scattered across every consumer
  • You can test the absence case directly, instead of hoping each caller handles it correctly

Key takeaways

  • Null objects replace nil with a real object that answers the same messages
  • They are useful when “nothing” is a valid business state, not an error
  • They work best when callers only need safe defaults, not a special failure path
  • They are not a substitute for exceptions when the absence really is a bug

A good null object keeps callers simple. Instead of repeating if value.nil? checks everywhere, you make the object itself decide what to return. That means the calling code can stay focused on the main flow, while the null implementation quietly handles the empty case.

That design is especially handy in presenters, loggers, mailers, and optional associations. In all of those places, the absence of an object is expected often enough that branching at every call site becomes noise.

A simple example

Imagine a Greeter class that formats greetings:

class Greeter
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end
end

Now imagine some callers receive nil for the name. Without a guard, calling greet on nil raises NoMethodError, which crashes the program. Every consumer of Greeter must remember to check for nil before making the call, and forgetting a single check means a runtime failure. You might write:

def display_greeting(greeter)
  return "No greeter provided" if greeter.nil?
  greeter.greet
end

The Null Object pattern tackles this from the opposite direction. Rather than teaching every caller to handle absence, you make absence itself behave correctly. You create a NullGreeter that satisfies the same interface, so callers never see nil in the first place:

class Greeter
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end
end

class NullGreeter
  def greet
    ""
  end
end

Notice that NullGreeter carries the empty-string default inside its own greet method. That detail matters: the behaviour for the missing case stays with the null object, not scattered across every method that might receive it. Now your factory decides which one to return:

def build_greeter(name)
  name ? Greeter.new(name) : NullGreeter.new
end

The factory method is the single decision point where the presence check happens. Every consumer gets back an object that implements greet, so no consumer has to worry about what kind of object it received or whether the name was provided. Calling code no longer checks for nil:

greeter = build_greeter(user_name)
puts greeter.greet  # always safe; no nil check needed

Implementing null objects in Ruby

The key contract is respond to the same messages. The null version should return values that are truthy or harmless; never ones that cause further errors downstream. In other words, callers should be able to invoke any method on the null object and get back a sensible result without branching or guarding.

Ruby gives you several ways to create null objects, each suited to a different situation. The approach you pick depends on how many methods the interface has and whether you want compile-time safety or runtime duck-typing. The implementations below show how Structs and modules each help maintain that contract with minimal boilerplate.

Struct-based null objects

Ruby Struct makes null objects almost free. With Struct, you get attr_reader accessors for free, so the null version automatically mirrors the attribute surface of the real object. You can also pass default values into the struct constructor, which makes it easy to supply sensible fallback data for display purposes. Define the interface once:

class User
  attr_reader :name, :email

  def initialize(name:, email:)
    @name = name
    @email = email
  end

  def full_identifiers
    "#{name} <#{email}>"
  end
end

NullUser = Struct.new(:name, :email) do
  def full_identifiers
    ""
  end
end

def find_user(id)
  # ...database lookup...
  id == 0 ? NullUser.new("Anonymous", "noreply@example.com") : User.new(name: "Alice", email: "alice@example.com")
end

Every caller that works with User also works with NullUser, since the struct shares the same attr_readers.

Module-based shared interface

When the interface has many methods, sharing it through a module makes the contract explicit and gives both the real and null implementations a common ancestor. This is especially helpful when you have multiple null variants that all need to satisfy the same set of methods. Share the interface with a module:

module Billable
  def base_rate
    raise NotImplementedError
  end

  def discount_eligible?
    raise NotImplementedError
  end

  def billable_hours
    raise NotImplementedError
  end
end

class StandardPlan
  include Billable

  def base_rate
    99.0
  end

  def discount_eligible?
    false
  end

  def billable_hours
    0
  end
end

class NullPlan
  include Billable

  def base_rate
    0.0
  end

  def discount_eligible?
    false
  end

  def billable_hours
    0
  end
end

Both classes satisfy the same module interface. Callers can invoke any Billable method without checking for nil. The NotImplementedError in the module acts as a self-documenting checklist: any class that includes Billable must provide every method, making it impossible to ship a half-finished null object.

Null collections

A common source of nil checks is code like:

users = fetch_users(user_id)
return if users.nil?
users.each { |u| puts u.name }

Instead of pushing nil-checking responsibility onto every caller, make the data layer guarantee it never returns nil. When the data source returns zero rows, the method should return an empty array rather than nil, so callers can safely iterate without a guard clause. This shifts the contract from “caller must check” to “method always delivers an iterable”:

def fetch_users(user_id)
  results = DB.query("SELECT * FROM users WHERE group_id = ?", user_id)
  results.map { |row| User.new(row) }
  # Always returns an Array, even if DB returns nothing
end

An empty array works for iteration, but some callers expect named methods like first, empty?, or to_a without checking whether they received an Array or some other enumerable. When callers depend on a fuller collection API, a purpose-built null collection object gives you control over every response. If you need a richer null collection that still quacks like a real one:

class NullCollection
  def each
    return enum_for(:each) unless block_given?
    # no-op: empty iteration
  end

  def to_a
    []
  end

  def first
    nil
  end

  def empty?
    true
  end

  def respond_to?(method, *)
    method == :each || super
  end
end

Callers can iterate, call empty?, or call first without any nil guards. This approach works well when the collection abstraction is the main thing callers care about and individual element access is secondary.

Ruby’s safe navigation operator as an alternative

Ruby 2.3 introduced the safe navigation operator &., which short-circuits method calls on nil. It is a lighter-weight tool that sits between raw nil checks and a full null object:

user = User.find_by(email: "nobody@example.com")
# => nil

user&.name
# => nil

user&.profile&.avatar&.url
# => nil, no NoMethodError, no stack trace

&. is useful when you genuinely do not know whether a value exists and you want a short expression to fall through to nil. It shines for deep attribute chains.

However, &. only solves this method call. It does not eliminate the problem; you still need to handle nil at some point. If you find yourself chaining six &. operators or wrapping everything in || nil, the Null Object pattern is the better answer.

Use &. for shallow, one-off nil checks. Use Null Objects when nil is a first-class business case with its own set of behaviours.

The nil?.present? pattern

A common Ruby idiom combines nil? and present? (from Active Support):

name = params[:name]
return "Unknown" if name.nil? || name.blank?

name.upcase

present? returns false for nil, false, empty strings, and empty collections. It is useful for gating logic, but it is still a conditional. If you need this gate in many places, a Null Object with sensible defaults is cleaner:

class NullName
  def upcase
    "UNKNOWN"
  end

  def blank?
    true
  end
end

Now the caller simply calls name.upcase and the NullName handles the “unknown” case in one place.

ActiveSupport::NilPlaceholder

Rails’ ActiveSupport::NilPlaceholder is a utility class specifically designed for cases where you need to represent “no value” but calling methods on it should return the placeholder itself, enabling chaining:

require "active_support/all"

result = ActiveSupport::NilPlaceholder.new

result.foo
# => nil

result.foo.bar
# => nil, no NoMethodError

This is useful when passing objects into views or templates where calling undefined methods should silently return nil rather than raising exceptions. However, for domain logic where you want specific “nothing” behaviour, such as an empty string, zero, or an empty array, a custom null object with defined return values is more expressive.

Practical examples

Null logger

Logging is everywhere. Passing a real logger in tests or CLI scripts is noise. A Null Logger solves this:

class NullLogger
  def debug(*); end
  def info(*); end
  def warn(*); end
  def error(*); end
  def unknown(*); end
  def add(*); end
  def <<(msg); end
end

class ApplicationService
  def initialize(logger: NullLogger.new)
    @logger = logger
  end

  def run
    @logger.info "Starting process"
    # ... work ...
    @logger.info "Finished"
  end
end

In production you pass Logger.new(STDOUT). In tests you pass NullLogger.new. The service never branches on which logger it received; the dependency injection framework or initializer handles the choice once, and every @logger.info call thereafter is safe.

Null mailer

Email dispatchers often need to be disabled in testing or development, and a null mailer avoids the need to stub or mock the delivery layer in every test:

class NullMailer
  def deliver(_message)
    # no-op
  end

  def deliver_now(_message)
    # no-op
  end

  def deliver_later(_message)
    # no-op; fire and forget
  end
end

class OrderConfirmation
  def initialize(mailer: NullMailer.new)
    @mailer = mailer
  end

  def send_confirmation(order)
    @mailer.deliver_now(build_email(order))
  end
end

Null presenter

Presenters wrap domain objects for view rendering, translating raw model attributes into display-ready values like formatted dates and profile URLs. When a record is missing; a logged-out visitor or a deleted author, for example; a Null Presenter supplies safe defaults so views never branch on presence:

class UserPresenter
  def initialize(user)
    @user = user
  end

  def display_name
    @user.name
  end

  def avatar_url
    @user.avatar.url
  end

  def member_since
    @user.created_at.strftime("%B %Y")
  end
end

class NullPresenter
  def display_name
    "Guest"
  end

  def avatar_url
    "/images/default-avatar.svg"
  end

  def member_since
    "—"
  end
end

def present_user(user)
  user ? UserPresenter.new(user) : NullPresenter.new
end

Views can call present_user(order.user).display_name without any || "Guest" inline.

When to use null objects vs raising exceptions

Null Objects and exceptions solve different failure modes. Use this as a guide:

SituationApproach
”I cannot find this record” is a normal, expected outcomeNull Object
The data being missing is an error conditionRaise nil → raise an exception
”Nothing here” is a valid business stateNull Object
Calling code asked for something that does not existRaise nil → raise an exception
You always expect a valid object and absence is a bugRaise an exception
Absence is a first-class case with defined behaviourNull Object

The Null Object pattern is not a replacement for proper error handling. If find_user failing to find a user is genuinely exceptional, raise an exception. If “no user found” is a valid state that calling code should handle gracefully, a Null Object is the right tool.

The key question to ask is: is the absence of this object a valid business state with its own behaviour? If yes, reach for null objects. If no, raise an exception.

Frequently asked questions

When should I choose a null object over a plain nil?

Choose a null object when callers need a predictable response and the empty case is part of normal business logic. If a missing value should still let the program continue, a null object is often cleaner than repeated nil checks.

Can a null object return nil?

Yes, but be deliberate. A null object should usually return safe defaults like an empty string, zero, or an empty collection. Returning nil is fine when that is the most honest representation of the missing value, but do not use it just because it is convenient.

What is the biggest risk?

The biggest risk is hiding a real error behind a harmless default. If absence should stop the flow or trigger a warning, use an exception instead of a null object.

Summary

Null objects replace scattered nil checks with a single object that handles the empty case in one place. That makes callers smaller, keeps branches out of the middle of your logic, and gives you a clear place to define what “nothing” should look like.

Use the pattern when the missing case is ordinary and expected. If the missing case is a problem you want to notice immediately, raise an exception and let the failure be visible.

See Also