Clean Code says "don't return null" and "don't pass null". What problems does null cause, and what should you return or accept instead?
answer
- Null = untyped 'nothing' the compiler doesn't warn about
- Return empty collection, never null list
- Special Case / Null Object = real object, harmless behaviour
- Optional in signature = absence you can't forget
- Null argument = caller bug -> fail fast at the boundary
basics
~20 sReturning null forces every caller to add a null check; one forgotten check crashes at runtime, far from the cause. Instead return an empty collection, a default "do-nothing" object (Null Object), or an explicit optional type. Never pass null as an argument.
solid answer
~50 sNull is an untyped 'no value' that every reference type silently accepts, so the compiler can't tell a legitimate value from a missing one. A method that returns null exports a maintenance obligation to every call site; miss one and you get a NullPointerException at the point of *use*, not the point of *creation*, which makes it hard to trace. Better alternatives, roughly in order: return an empty collection instead of null for list-shaped results (callers can iterate unconditionally); return a **Special Case** or **Null Object** — a real instance of the expected type whose behaviour is the harmless default (e.g. a `NoSuchEmployee` whose `getPay()` returns zero); or return an explicit optional/Result type so absence is visible in the signature and the compiler forces you to unwrap it. Passing null is worse: the callee cannot do anything sensible with it, so it should reject it at the boundary via assertion or precondition check — fail fast — rather than sprinkle defensive checks.
code
pseudocode · 17 lines// smell: caller must guard, and someone eventually won't
List<Item> items = repo.findItems(q);
if (items != null) { for (Item i : items) total += i.price(); }
// fix 1: empty collection
List<Item> items = repo.findItems(q); // never null
for (Item i : items) total += i.price();
// fix 2: Special Case object
Employee e = repo.findEmployee(id); // returns NoEmployee if absent
total += e.getPay(); // NoEmployee.getPay() == 0
// fix 3: fail fast on a null argument instead of defending everywhere
void register(Customer c) {
require(c != null, "customer must not be null");
...
}go deeper
Say null forces checks everywhere, one miss = NPE, and give the two concrete fixes: return an empty collection, and validate arguments instead of accepting null.
Name the patterns (Special Case / Null Object), contrast with Optional/Result, and explain fail-fast on null arguments at the module boundary rather than defensive checks throughout.
Discuss where each option belongs by layer, the danger of Null Objects hiding real absence, null vs empty vs absent in API/DB semantics, and root-cause fixes via non-nullable types / strict null checking.
Treat it as a codebase-wide policy: nullability annotations or a language with non-nullable defaults, enforced by CI static analysis, plus a consistent house rule for absence in public contracts (empty collections, documented optional fields, PATCH semantics) so teams don't each invent one.
## What null actually is In most object-oriented languages a variable of reference type can hold either an object or the special value `null` / `nil` / `None`, meaning "no object here". Tony Hoare, who introduced it in 1965, later called it his "billion-dollar mistake" — precisely because the type system permits it everywhere without saying so. `Customer c` claims to be a customer; at runtime it might be nothing at all, and you only find out when you touch it: NullPointerException, `TypeError: Cannot read property of null`, segfault. ## Why *returning* null is a design smell When a method returns null it is quietly amending its contract to "…or nothing", and it delegates the handling to every caller, forever. ``` List<Employee> employees = getEmployees(); for (Employee e : employees) { total += e.getPay(); } // NPE if getEmployees() returns null ``` Three concrete costs: 1. **Clutter.** Real code degenerates into `if (x != null && x.getY() != null && ...)`. The null checks outnumber the logic, exactly the disease error codes cause. 2. **Ignorability.** Nothing in the signature or the compiler reminds you. One missed check is a production crash. 3. **Distance between cause and symptom.** The null was *created* in a repository at 09:00 and *dereferenced* in a formatter three layers away. The stack trace points at the innocent party. ## The replacements **Empty collections.** For anything list/set/map/array shaped, return the empty one. `for (x : emptyList)` runs zero times and needs no guard. This single rule removes a large share of real-world null checks. Prefer a shared immutable empty instance so it costs nothing. **Special Case / Null Object pattern.** Introduce a subtype (or a sibling implementation of the interface) that represents the missing case with harmless behaviour: ``` interface Employee { Money getPay(); boolean isPayable(); } class NoEmployee implements Employee { Money getPay() { return Money.ZERO; } boolean isPayable() { return false; } } ``` The caller writes `total += repo.findEmployee(id).getPay();` with no branch at all. This is Fowler's **Special Case** pattern; the degenerate "does nothing" version is the **Null Object** pattern (Woolf). It is at its best when there genuinely *is* a sensible neutral behaviour: a `NullLogger` that discards, a `GuestUser` with no permissions, a `NoOpMetrics`. *Caveat:* it is at its worst when absence should change the outcome. A Null Object that silently returns zero can make a bug invisible — the order is charged $0 and nobody notices. Use it where "do nothing" is genuinely correct, not to sweep the missing case under the rug. **Explicit optional types.** `Optional<T>` / `Maybe` / `T?` puts the absence *in the type*, so the compiler or linter forces you to deal with it. Two things matter here: (a) use them as *return* types, not as fields or parameters, and (b) they don't remove the branch — they make it impossible to forget. Languages with non-nullable types by default (Kotlin, Swift, Rust, TypeScript with strictNullChecks, modern C# NRTs) solve the problem at the root: `String` cannot be null, `String?` must be unwrapped. **Throwing.** If absence is truly exceptional — the ID came from a foreign key that must exist — throw a domain exception instead. Clean Code's own preference for "unusual event" cases. ## Why *passing* null is worse If you return null, at least the caller can react. If you *pass* null into a method, that method now has to invent a policy for an argument that shouldn't exist: - Add a null check and silently do nothing → hides a caller bug. - Add a null check and throw → reasonable, but every method pays the tax. - Don't check → NPE deep inside, with a useless stack trace. The clean stance: **treat null arguments as programmer errors and fail fast.** Validate at the public boundary (`requireNonNull(x, "x")`, `assert`, a precondition helper), so the failure names the offending parameter and points at the caller. Inside the module, once the boundary is guarded, stop checking. Better still, design so null can't be passed: overloads instead of an optional argument, a non-nullable type, or a Null Object the caller can pass explicitly. ## Practical migration order 1. Turn on the compiler/analyzer setting that makes nullability explicit (strict null checks, nullability annotations, `@NonNull` defaults). 2. Make every collection-returning method return empty, never null. 3. Guard public entry points with precondition checks. 4. Introduce Special Case objects where a neutral behaviour exists; Optional/Result where absence is a real branch. 5. Delete the defensive checks that are now provably unreachable. ## Edge cases people trip on - **Null vs empty vs absent are three different things.** A missing field, a field explicitly set to null, and an empty string are distinct in JSON APIs and in databases. Collapsing them loses information (e.g. PATCH semantics: "don't change" vs "clear this field"). - **SQL NULL is not object null**: it means unknown, and it propagates through comparisons (`NULL = NULL` is not true). - **Optional in fields/parameters** adds an allocation and an extra unwrap without adding safety; most style guides restrict it to return types. - **Null Object plus equality**: make sure the special case compares and serializes sensibly, or it leaks into places that don't expect it.
- When is a Null Object the wrong choice?When absence must change the outcome or be reported. A NullCustomer whose getDiscount() returns 0 turns a lookup bug into a silently wrong invoice. Use it only where 'do nothing / neutral value' is a genuinely correct behaviour (loggers, metrics, default policies); otherwise use Optional/Result or throw so the caller must decide.
- Doesn't Optional just move the null check somewhere else?It moves the branch from something you can forget to something the type system makes you handle — that's the whole point. It also documents intent in the signature. It does not make code branch-free, and wrapping fields or parameters in Optional adds cost without safety, so restrict it to return types.
- If a method's argument is legitimately optional, how do you avoid passing null?Provide overloads or a builder so the caller simply omits it, pass an explicit Null Object / default value, or use a language-level optional parameter. Null as 'I didn't want to supply this' is a missing abstraction, not a value.
Returning null is like handing someone an unlabelled box that might be empty: they must shake every box before opening it. A Null Object is handing them a box with a clearly-marked dummy inside — they can process it the same way as any other and nothing explodes.
saying these in an interview costs you the question
- "I return null and document it" — documentation is not enforced; the compiler is
- Wrapping every dereference in defensive null checks instead of fixing the source
- Using a Null Object where the missing case must be reported, so bugs return quietly wrong numbers
- Declaring fields and parameters as Optional to 'be safe'
- Treating null, empty and absent as interchangeable in an API payload
- Catching NullPointerException as an error-handling strategy