Here is the article content with 3 internal links added:

Actually, I should clarify – I honestly thought we were done with this conversation. Yet here I am, staring at a pull request in 2026, arguing about null checks again. It seems like every six months, the collective developer consciousness remembers the Null Object pattern exists, gets excited, overuses it, and then forgets it again. Rinse, repeat.

But last Tuesday, I hit a breaking point.

I was refactoring a legacy payment gateway integration—running on Node.js 23.4, if you’re curious—and the code was absolutely riddled with defensive checks. You know the type. The kind that makes you want to close your laptop and go become a goat farmer.

if (logger !== null && logger !== undefined) {
    logger.log("Processing payment...");
}

// ten lines later...
if (metrics !== null) {
    metrics.increment("attempts");
}

// another twenty lines...
if (logger !== null) {
    logger.log("Done.");
}

It’s noise. Pure, unadulterated visual noise. And it distracts from the actual business logic, which is just “process the payment.”

The “Do Nothing” Strategy

So, I did what any annoyed senior engineer would do: I ripped it all out and replaced it with a Null Object. If you’ve been living under a rock (or just exclusively writing Rust), the idea is stupidly simple. Instead of passing null when you don’t have a logger, you pass an object that looks like a logger but does absolutely nothing.

It’s polymorphism 101, but for some reason, we keep forgetting it applies to “absence” too.

Here is the fix I pushed. No libraries, no complex generic types, just a class that implements the interface and does zip.

interface Logger {
    log(message: string): void;
    error(message: string): void;
}

class ConsoleLogger implements Logger {
    log(msg: string) { console.log(msg); }
    error(msg: string) { console.error(msg); }
}

class NullLogger implements Logger {
    log(msg: string) { /* do nothing */ }
    error(msg: string) { /* do nothing */ }
}

// Usage
const logger = config.enableLogs ? new ConsoleLogger() : new NullLogger();

// No more checks!
logger.log("This line is safe now.");

The beauty here isn’t just that the if statements are gone. It’s that the consuming code doesn’t care. It trusts that the dependency exists.

Why Is This controversial in 2026?

You’d think this is settled law, but the debate I saw raging on social media this week proves otherwise. The modern counter-argument usually screams: “Just use Optionals!” or “Use strict null checks!”

Look, I love TypeScript’s strict null checks. But types only tell you that something is missing; they don’t handle the behavior of what to do when it is.

If I use an Optional (or logger?: Logger), I still have to unwrap it. I still have to branch.

logger?.log("Hello") is cleaner, sure. But it falls apart when you have complex interactions.

The Danger Zone: Data vs. Behavior

I once tried to use Null Objects for data entities—like a NullUser instead of null for a missing database record. Disaster. Absolute disaster.

My rule of thumb:

  • Use Null Object for behavior (services, strategies, commands, loggers).
  • Use Nullable types / Optionals for data (users, records, configuration values).

Performance: The Hidden Gotcha

There’s a specific edge case I ran into last year working on a high-throughput financial engine (handling about 12k requests/sec). We replaced a null check with a Null Object in a tight loop.

Surprisingly, performance dropped. Not by much—maybe 3%—but enough to trip our regression alerts.

I ended up reverting that specific change. Sometimes, ugly code is fast code.

The Verdict in 2026

The Null Object pattern isn’t “modern” or “legacy.” It’s just a tool for removing branching logic. If you find yourself writing the same if (x != null) check in three different places for the same object, you are wasting your time and your reader’s cognitive load.

I’m betting we’ll still be having this argument in 2030. But for now, check your codebase. If you see a service that is optional, don’t sprinkle ?. everywhere. Just give it a default implementation that does nothing. Your unit tests will thank you—mocking becomes trivial when the default is “safe.”

Now, if you’ll excuse me, I have to go explain to a junior dev why Optional<Optional<String>> is a crime against humanity.