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
nilcheck 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
nilwith 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:
| Situation | Approach |
|---|---|
| ”I cannot find this record” is a normal, expected outcome | Null Object |
| The data being missing is an error condition | Raise nil → raise an exception |
| ”Nothing here” is a valid business state | Null Object |
| Calling code asked for something that does not exist | Raise nil → raise an exception |
| You always expect a valid object and absence is a bug | Raise an exception |
| Absence is a first-class case with defined behaviour | Null 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
- /guides/ruby-service-objects/: Service objects complement Null Objects well; both move branching logic out of callers
- /guides/ruby-value-objects/: Value Objects and Null Objects both encapsulate behaviour around data representations
- /guides/ruby-struct-guide/: Struct is often the fastest path to a Null Object implementation in Ruby