Symptom

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.

Goal

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() { /* ... */ }
}
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 9. The book uses a Customer hierarchy with a NullCustomer; this JavaScript version uses NoDiscount and NoLoyaltyProgram — same pattern, scaled to two collaborators to show how callers stop branching.
Pressure

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.

Tradeoff

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.

Relief

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.

Trap

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.