The idea
What it is
Null Object answers one question: what should a method return when there's nothing to return? Picture a coffee-shop loyalty system. shop.lookupCustomer(cardId) finds the customer for a swiped card, and checkout applies their plan's discount with one tidy chain: customer.getPlan().getDiscount(). Then a walk-in arrives with no card. The naive answer — return null — leaves the caller holding a live grenade: forget one check and .getPlan() on null crashes the register.
So teams defend themselves the only way null allows: if (customer != null && customer.getPlan() != null) { … } — copied into every method that touches a customer. The Null Object pattern deletes that entire tax. When there's no real customer, lookupCustomer returns a GuestCustomer: a genuine object with the same interface, whose plan is a NoDiscountPlan — discount 0%, perks empty, side effects none. The chain runs end to end for everyone, and the 'nobody' case is handled once, inside the null object, instead of everywhere it's used.
The one sentence to remember
Replace null with a real object of the same interface that safely does nothing — neutral values, no-op methods — so callers never have to check for absence.
The billion-dollar mistake
Tony Hoare, who introduced the null reference in 1965, later called it his billion-dollar mistake for all the crashes and defensive code it has caused since — Null Object is one of the classic ways to design that mistake out of your APIs.
Mechanics
How it works
The three participants
The pattern is tiny — an interface and two kinds of implementation of it:
- The abstract type — the interface callers program against:
Customer(withgetPlan()) andPlan(withgetDiscount(),perks()). Callers only ever see this type, never a concrete class. - Real implementations — the objects with actual behaviour: a
MemberCustomerwhoseGoldPlanreturns20% off and real perks. - The Null implementation —
GuestCustomerandNoDiscountPlan: same interface, neutral behaviour. Numbers return0, collections return empty (nevernull), actions are no-ops. It does nothing — safely.
Because a null object holds no state and always behaves the same, you usually create one shared instance and hand it out everywhere — a natural little [[singleton]]. There's no point allocating a fresh NoDiscountPlan per walk-in when every one of them is identical.
interface Plan {
getDiscount(): number; // percent off
}
class GoldPlan implements Plan {
getDiscount() { return 20; } // real behaviour
}
class NoDiscountPlan implements Plan { // the Null Object
static readonly INSTANCE = new NoDiscountPlan(); // stateless → share one
getDiscount() { return 0; } // neutral: safely does nothing
}
// lookupCustomer() NEVER returns null — walk-ins get a GuestCustomer
// whose getPlan() is NoDiscountPlan.INSTANCE. So checkout is one line:
const price = 10 * (1 - shop.lookupCustomer(card).getPlan().getDiscount() / 100);Where it shines
- Chained calls —
a().b().c()is only safe if no link can benull. Null objects keep the whole chain alive with neutral values, exactly like the checkout line above. - Collections mixing real and 'missing' entries — loop over
customersand callgetPlan().getDiscount()on every element without filtering; guests simply contribute0. - Default collaborators — give a class a
NullLoggerby default so its code callslogger.log(...)unconditionally; wire in a real logger only when you want output.
The honest caveat: it can hide real errors
A null object swallows absence. That's the feature — and the danger. If 'missing' is a genuine error the caller must know about (an unknown payment account, a required config key), a silent do-nothing object turns a loud crash into a quiet wrong answer that surfaces days later. Reserve the pattern for cases where doing nothing is correct, not merely convenient.
The modern spectrum for 'maybe absent'
Null Object is one point on a spectrum. Java's Optional<Customer> and TypeScript's Customer | undefined with optional chaining (customer?.getPlan()) make absence visible in the type and force callers to decide; a null object makes absence invisible and decides for them. Use the type-level tools when callers should care, and a null object when they genuinely shouldn't.
You've already met this pattern in the wild: a NullLogger (log calls go nowhere), the guest user/session every web framework hands to signed-out visitors, a no-op cache used when caching is disabled, and the empty iterator — the reason for (x of emptyList) just runs zero times instead of crashing.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A coffee-shop checkout lane. The CODE card shows the one chained line every checkout runs: lookupCustomer(card).getPlan().getDiscount(). Flip between two worlds and swipe. In WITHOUT Null Object, 💳 Member card works fine ($10.00 → $8.00 ✓), but 🪪 No card makes the lookup return a red null — the chain reaches .getPlan() and 💥 explodes with a NullPointerException. Press 🩹 Add null checks and the tidy one-liner turns into a red 5-line if-pyramid (checks: 2) that survives the swipe — that's the tax null makes you pay in every method. Flip to WITH Null Object: 🪪 No card now returns a calm grey GuestCustomer with a NoDiscountPlan, the same one-line chain runs end-to-end green ($10.00 → $10.00 ✓, checks: 0), and no crash is possible. Same code line, no ifs — that's the whole pattern.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Crash the WITHOUT world
With the toggle on ✕ WITHOUT Null Object, press 💳 Member card — the lookup returns a gold Customer, the chain lights up left to right, and the price drops $10.00 → $8.00 ✓. Now press 🪪 No card: the lookup returns a red null token, the pulse reaches .getPlan() and 💥 explodes — NullPointerException, price ☠. Same line of code, one missing card, dead register.
Pay the null-check tax
Press 🩹 Add null checks. The tidy one-liner on the CODE card turns into a red 5-line if-null pyramid and the counter jumps to checks: 2. Swipe 🪪 No card again — it survives now, but look at what checkout code became. Multiply that pyramid by every method that touches a customer: that's the tax null makes the whole codebase pay.
Flip to WITH and compare
Switch the toggle to ✓ WITH Null Object — the code is the original one-liner again and the counter reads checks: 0. Press 🪪 No card: the lookup returns a calm grey GuestCustomer with a NoDiscountPlan, the same chain runs end-to-end green, and the price stays $10.00 ✓. Press 💳 Member card to confirm members still get $8.00, then ↺ Reset and replay both worlds — identical code line, zero ifs, no crash possible.
In practice
When to use it — and what trips people up
Use it when doing nothing is the correct answer
- A sane do-nothing default exists — 0% discount, empty perks, a log call that goes nowhere. If the neutral behaviour is genuinely right for the missing case, a null object encodes it once.
- Callers shouldn't care about absence — checkout should not branch on 'is this a member?'; it should just ask the plan for a discount and get
0for guests. - Chained calls and collection loops — anywhere
nullwould force a guard before every dot or a filter before every loop. - Optional collaborators — loggers, metrics, caches, notifiers that a class should be able to call unconditionally whether or not one is configured.
Skip it when absence is an error
- The caller must know something is missing — an unknown payment account, a required config key, a failed lookup that should stop the flow. A silent no-op here converts a loud, debuggable crash into quiet wrong behaviour.
- Different callers need different reactions to absence — one wants a retry, one wants an error page. A null object bakes in a single reaction (nothing); prefer
Optional/Maybetypes or explicit| undefinedhandling so each caller decides. - You'd need the null object to lie — if
GuestCustomerhad to fake a name, an ID, and an address to satisfy the interface, the 'neutral' behaviour isn't neutral anymore. That's a sign the design wants explicit absence.
What it gives you
- Deletes repetitive null checks — the 'nobody' case is implemented once, inside the null object, instead of guarded before every call site.
- Makes chained calls and loops safe by construction — every link in
a().b().c()is a real object, so the happy path is the only path. - Callers get simpler and polymorphic — checkout treats members and guests identically through one interface, with no
if (guest)branching. - Cheap to share — null objects are stateless, so one instance (a mini [[singleton]]) serves the whole program with zero allocation cost.
Common mistakes
- Can hide genuine errors — a silent no-op where absence mattered turns crashes into quiet wrong results that are much harder to trace.
- One extra class per interface — for a type with many methods, writing a neutral version of every method is real boilerplate.
- One-size-fits-all reaction — every caller gets 'do nothing'; if different callers need different absence handling, the pattern is the wrong tool.
- Can mask design smells — sprinkling null objects everywhere to keep long
a().b().c()chains alive papers over coupling that violates the Law of Demeter.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
interface Plan {
getDiscount(): number; // percent off, e.g. 20
perks(): string[];
}
class GoldPlan implements Plan {
getDiscount() { return 20; }
perks() { return ["free refill", "birthday drink"]; }
}
// The Null Object: same interface, neutral behaviour.
class NoDiscountPlan implements Plan {
static readonly INSTANCE = new NoDiscountPlan(); // stateless → share one
getDiscount() { return 0; } // neutral number
perks(): string[] { return []; } // empty, never null
}
interface Customer {
getName(): string;
getPlan(): Plan;
}
class MemberCustomer implements Customer {
constructor(private name: string) {}
getName() { return this.name; }
getPlan(): Plan { return new GoldPlan(); }
}
class GuestCustomer implements Customer { // Null Object for Customer
getName() { return "Guest"; }
getPlan(): Plan { return NoDiscountPlan.INSTANCE; }
}
class Shop {
private members = new Map<string, Customer>([["gold-7", new MemberCustomer("Mia")]]);
lookupCustomer(cardId?: string): Customer { // NEVER returns null
return (cardId && this.members.get(cardId)) || new GuestCustomer();
}
}
// Checkout: one line, zero null checks, works for members AND walk-ins.
const shop = new Shop();
const price = 10 * (1 - shop.lookupCustomer("gold-7").getPlan().getDiscount() / 100);
console.log(price); // 8 — guest would print 10References & further reading
5 sources- Articlerefactoring.guru
Introduce Null Object — Refactoring.Guru
The refactoring recipe: how to spot repeated null checks and mechanically replace them with a null object, with before/after code.
- Docsen.wikipedia.org
Null object pattern — Wikipedia
Concise reference with the pattern's structure and side-by-side examples in a dozen languages, plus its relationship to Optional types.
- Articlesourcemaking.com
Null Object — SourceMaking
Walks the intent, structure diagram, and check-list for deciding whether a do-nothing default is safe, with a NullLogger-style example.
- Talkinfoq.com
Null References: The Billion Dollar Mistake — Tony Hoare (InfoQ talk)
The inventor of the null reference explains why he regrets it — the historical 'why' behind every pattern and type-system feature that fights null.
- Booken.wikipedia.org
Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides
Null Object isn't in the original GoF catalogue (Bobby Woolf described it later in 'Pattern Languages of Program Design 3'), but this book defines the pattern vocabulary and polymorphism ideas it builds on.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What is the core idea of the Null Object pattern?
question 02 / 05
Why does shop.lookupCustomer(card).getPlan().getDiscount() never crash once GuestCustomer and NoDiscountPlan exist?
question 03 / 05
NoDiscountPlan holds no state and always behaves the same. What does this suggest about how to create it?
question 04 / 05
What is the main danger of using a Null Object, shown by the honest caveat in this lesson?
question 05 / 05
When should you prefer Java's Optional or TypeScript's | undefined with optional chaining over a Null Object?
0/5 answered