# Introduce Null Object

> Replace repeated null checks for a missing collaborator with an object that implements the same interface and does the safe default.

- kind: technique
- category: simplifying-conditional-expressions
- aliases: Null Object
- canonical: https://aipatterns.dev/refactoring/techniques/introduce-null-object
- inspired-by: https://refactoring.guru/introduce-null-object

## Problem

Many callers check whether an optional collaborator exists before using it, and each supplies the same fallback: skip the call, use zero, use "Guest". The checks are copied across the codebase, and one missing check is a crash.

## Solution

**Create a class that implements the collaborator's interface with neutral behavior, and return or inject it wherever `null` was used.** Callers then call methods without checking.

## Before

```ts
interface Analytics {
  track(event: string, props: Record<string, unknown>): void;
  flush(): Promise<void>;
}

class CheckoutService {
  constructor(private analytics: Analytics | null) {}

  async complete(order: Order): Promise<void> {
    await this.payments.capture(order);
    if (this.analytics) {
      this.analytics.track("checkout_completed", { total: order.total });
    }
    await this.emails.sendReceipt(order);
    if (this.analytics) {
      await this.analytics.flush();
    }
  }
}
```

## After

```ts
class NoopAnalytics implements Analytics {
  track(): void {}
  async flush(): Promise<void> {}
}

class CheckoutService {
  constructor(private analytics: Analytics = new NoopAnalytics()) {}

  async complete(order: Order): Promise<void> {
    await this.payments.capture(order);
    this.analytics.track("checkout_completed", { total: order.total });
    await this.emails.sendReceipt(order);
    await this.analytics.flush();
  }
}
```

## Why

- The default behavior is defined once, inside the null object, instead of at every call site.
- Callers become straight-line code.
- Tests can pass the null object explicitly to switch off a dependency.

## Steps

1. Confirm that callers handle the missing case the same way everywhere. Note the fallback value or action.
2. Create the null class implementing the same interface, with methods that return the fallback. Run the tests.
3. Change the source of the value (constructor default, factory, repository lookup) to return the null object instead of `null`. Keep the old checks in place; they now see a non-null object and still work. Run the tests.
4. Remove the checks one call site at a time. Run the tests after each.
5. If some caller needs to know the object is "missing", add an `isNull()` method or keep that caller's check against the type, but try to avoid it.

## Watch out

- Do not use a null object when absence is an error. A missing payment gateway should fail loudly; a silent no-op hides misconfiguration.
- Do not use it when callers react differently to absence. Then the null object cannot pick one right default, and an `Option`/`undefined` with explicit handling is clearer.
- In TypeScript, Kotlin, Swift or C#, optional chaining (`analytics?.track(...)`) removes most of the noise for a few call sites. Use a null object when the fallback is more than "do nothing" or appears in many places.
- Make null objects stateless and shareable, or each instance quietly accumulates data.
- No inverse technique exists; to undo, reinstate `null` and the checks.

## Related

- Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md) in their `if (x == null)` form, and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md) in repeated fallbacks.
- A special case of [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md).
- A null object is often a do-nothing [Strategy](https://aipatterns.dev/design-patterns/strategy.md).
- When absence must never happen, use [Introduce Assertion](https://aipatterns.dev/refactoring/techniques/introduce-assertion.md) instead.

## Agent checklist

- [ ] Every replaced null check used the same fallback.
- [ ] Absence is a legitimate state, not a configuration error.
- [ ] The null object implements the full interface with neutral return values.
- [ ] Remaining `null` returns from the source are gone, and tests pass.
