Code is littered with `if (x !== null) x.do()` checks around the same collaborator. Each new caller adds another check; missing one ships a NullPointerException to production; the absence-handling logic duplicates across callers.
A Null Object implementing the same interface stands in for the missing collaborator. Callers invoke methods unconditionally; the Null Object returns sensible defaults (return the input unchanged, return zero, do nothing).
Before the refactoring
// Every consumer of Customer.discount and Customer.loyaltyProgram must check for null first.class Order {constructor(customer) {this.customer = customer;}finalPrice() {let price = this.subtotal();if (this.customer.discount !== null) {price = this.customer.discount.applyTo(price);}if (this.customer.loyaltyProgram !== null) {price -= this.customer.loyaltyProgram.pointsValue();}return price;}subtotal() { /* ... */ }}
After the refactoring
// Null Objects implement the same interface with safe defaults; callers stop branching.class NoDiscount {applyTo(price) {return price;}}class NoLoyaltyProgram {pointsValue() {return 0;}}class Order {constructor(customer) {// customer.discount and customer.loyaltyProgram are never null;// they are NoDiscount/NoLoyaltyProgram when absent.this.customer = customer;}finalPrice() {let price = this.subtotal();price = this.customer.discount.applyTo(price);price -= this.customer.loyaltyProgram.pointsValue();return price;}subtotal() { /* ... */ }}
Null checks duplicate at every call site; the rules for what 'absent' means duplicate too. Adding a new caller is one more place to remember the check; forgetting one is a runtime bug.
Null Objects hide absence; downstream code may mistake the silent no-op for a real operation, especially when the Null Object's behaviour drifts from the originally-intended default. Debugging 'why did nothing happen?' requires recognizing that a Null Object is in hand.
Call sites simplify to plain method calls; absence-handling lives in one place (the Null Object); the rules for what 'no discount' or 'no loyalty program' means are visible and testable as a single class.
A Null Object with subtly-wrong defaults silently corrupts results without throwing. The pattern pays when 'absent' has a well-defined neutral semantics; for collaborators where absence should actually halt the operation, Null Object hides the error the caller expected to see.