Why I Stopped Writing Null Checks
Null is the silent killer of software maintainability. It sneaks into your codebase as a harmless default but quickly grows into a menace that disrupts your logic, scatters your error handling, and complicates your flow.
For years, I wrote if (obj == null)
out of habit, thinking it was the responsible thing to do. But then I realized: null checks don’t solve problems—they reveal a lack of design.
By adopting Null Object Patterns and Result Types, I stopped fighting null and started designing around it. The result? Cleaner, more predictable code with less cognitive overhead.
Null Is a Symptom, Not the Problem
Null checks may seem like a defensive strategy, but they’re actually a sign of poor abstraction. Consider this example:
if (customer == null)
{
return "Customer not found.";
}
if (customer.Address == null)
{
return "Customer address is missing.";
}
return $"Shipping to {customer.Address}.";
This code works, but it’s fragile. What if more nullable properties are added? What if the Address
class gains its own nullable dependencies? The more null checks you add, the harder the code becomes to follow and maintain.
The real problem is that null isn’t being treated as a first-class concept. Instead of addressing it at the root, null checks scatter the responsibility across your codebase.
From Chaos to Clarity with Null Patterns and Results
The Null Object Pattern solves this by eliminating null as a valid state. Instead, you design objects to always provide meaningful behavior, even in their “default” state.
Let’s refactor the earlier example using this pattern:
public class NullCustomer : Customer
{
public override Address Address => new NullAddress();
}
var customer = GetCustomer() ?? Customer.Null;
Console.WriteLine($"Shipping to {customer.Address.Street}");
Here, we introduce NullCustomer
and NullAddress
to encapsulate default behavior. Instead of checking for null, the calling code assumes all objects are valid and behaves accordingly.
Advantages:
- Encapsulation: Default behavior is defined in one place, not scattered across the codebase.
- Simplified Logic: No need for repeated null checks.
- Readability: The calling code focuses on what it needs to do, not on handling edge cases.
Making Failure Explicit
Null checks often double as error-handling mechanisms, blurring the line between valid and invalid states. Result Types fix this by explicitly representing success and failure as first-class outcomes.
public class Result<T>
{
public T Value { get; }
public string Error { get; }
public bool IsSuccess => Error == null;
private Result(T value, string error)
{
Value = value;
Error = error;
}
public static Result<T> Success(T value) => new(value, null);
public static Result<T> Failure(string error) => new(default, error);
}
And here’s how you might use it:
var result = GetCustomer();
if (result.IsSuccess)
{
Console.WriteLine($"Customer: {result.Value.Name}");
}
else
{
Console.WriteLine($"Error: {result.Error}");
}
By wrapping the return value in a Result
, we remove ambiguity:
- If the operation succeeds, the value is guaranteed to be valid.
- If it fails, the error is explicit and cannot be ignored.
Combining Null Patterns and Results
These two concepts are not mutually exclusive. In fact, they work beautifully together.
var result = GetCustomer();
if (result.IsSuccess)
{
Console.WriteLine($"Shipping to {result.Value.Address.Street}");
}
else
{
Console.WriteLine($"Error: {result.Error}");
}
Here’s what happens:
- The Result Type communicates success or failure at the boundary of the method.
- The Null Object Pattern ensures that even in edge cases, the objects used downstream are valid.
For the end
This combination creates a clear, predictable flow of data through your system. Each layer knows exactly what to expect, reducing cognitive load and eliminating edge cases.
The decision to stop writing null checks wasn’t just about aesthetics; it was about embracing a design philosophy. Instead of fighting null, I started designing for clarity and intent:
- Null Object Patterns eliminate null as a valid state, making objects reliable by design.
- Result Types express failure explicitly, ensuring that success and error handling are clearly separated.
These patterns didn’t just clean up my code — they changed the way I think about software. Instead of writing defensive code, I now write deliberate code.
So, the next time you write if (obj == null)
, ask yourself:
“What am I really trying to achieve here? Is there a better way to express this logic?”
Cheers and Good Luck! 👋