Sharing is caring… Especially with code

Null Isn’t Null: Understanding Null References and Defensive Programming

Written by

Have you ever checked that a variable isn’t null, only to have your code fail a few lines later?

It happens more often than you might think. Sometimes the problem is a missing object. Other times, the object exists, but one of its properties or fields contains a null reference. The distinction matters because checking one doesn’t automatically protect you from the other.

In this article, we’ll look at what null actually means in C#, why null checks don’t solve every problem, and how defensive programming can help make your code more reliable.

1. What Does Null Actually Mean?

In C#, null represents the absence of an object reference. It doesn’t mean an object exists but contains no data. It means the reference doesn’t point to an object.

Consider this example:

string name = null;

Console.WriteLine(name.Length);

The variable name exists, but it doesn’t refer to a string object. Trying to access its Length property causes a NullReferenceException.

The problem isn’t that the string is empty. An empty string is a valid string object:

string name = "";

Console.WriteLine(name.Length); // Outputs 0

These two examples look similar at first glance, but they represent different states.

An empty string exists. A null string reference does not point to a string object at all.

Understanding that difference is the first step toward writing safer code.

2. An Object Can Exist While Its Data Is Missing

Now let’s look at a slightly different situation.

public class Customer
{
    public string Name { get; set; }
}

We can create a customer object without assigning a name:

Customer customer = new Customer();

Console.WriteLine(customer.Name.Length);

The customer variable references a valid object. However, its Name property has the default value null, because we haven’t assigned a string to it.

The exception occurs when we access Length, not when we access the customer object itself.

This distinction is important:

  • The customer reference is not null.
  • The customer object exists.
  • The Name property is null.

A check of the customer reference alone won’t protect us from accessing a missing property value.

if (customer != null)
{
    Console.WriteLine(customer.Name.Length);
}

This check is valid, but it isn’t sufficient. The code can still throw a NullReferenceException because customer.Name is null.

The solution is to validate the value we intend to use, not merely the object that contains it.

if (customer != null && customer.Name != null)
{
    Console.WriteLine(customer.Name.Length);
}

C# evaluates the conditions from left to right. Because && uses short-circuit evaluation, the second condition is evaluated only if the first one is true.

That protects both accesses in this example.

3. Why Null Checks Aren’t a Complete Solution

It’s tempting to add null checks everywhere and assume the problem is solved. But defensive programming is about more than checking references.

Consider a method that processes an order:

public void ProcessOrder(Order order)
{
    if (order == null)
    {
        return;
    }

    // Continue processing the order.
}

This method protects itself against a null argument. But what should happen when the argument is null?

Simply returning might be correct if the method is designed to ignore missing optional input. If an order is required, silently returning could hide a programming error and leave the caller believing the order was processed.

The appropriate response depends on the method’s contract.

For required input, throwing an exception can make the problem explicit:

public void ProcessOrder(Order order)
{
    if (order == null)
    {
        throw new ArgumentNullException(nameof(order));
    }

    // Continue processing the order.
}

Now the method clearly communicates that a required argument is missing.

For optional input, a null check followed by a reasonable fallback might be more appropriate.

The point is not to eliminate every null reference by adding checks indiscriminately. The point is to decide what each situation means and handle it accordingly.

4. Guard Clauses Keep Code Easier to Read

One useful technique is the guard clause.

A guard clause checks whether a required condition has been met and exits the method early if it hasn’t.

For example:

public void PrintCustomerName(Customer customer)
{
    if (customer == null)
    {
        throw new ArgumentNullException(nameof(customer));
    }

    if (customer.Name == null)
    {
        throw new InvalidOperationException(
            "The customer does not have a name.");
    }

    Console.WriteLine(customer.Name);
}

Instead of nesting the main logic inside several conditions, we handle invalid states near the beginning of the method.

This makes the expected execution path easier to follow.

Notice that we’re checking two separate conditions. The first validates the customer reference. The second validates the name required by this particular operation.

That distinction is intentional.

Not every customer object necessarily needs a name in every application. If a name is optional, the second check should reflect that design rather than treating every missing value as an error.

Defensive programming should support the rules of the application, not invent new ones.

5. Sometimes You Should Recover Instead of Throwing

Not every unexpected value should stop the program.

Suppose an application reads a display name from a configuration file. If the name is missing, a default value might be perfectly acceptable.

string displayName = configuredName ?? "Guest";

The null-coalescing operator, ??, returns the value on the left when it isn’t null. Otherwise, it returns the value on the right.

This is useful when a fallback is genuinely appropriate.

However, defaults should be chosen carefully. Replacing a missing database connection string with an arbitrary value, for example, doesn’t solve the underlying problem. It merely moves the failure somewhere else.

A good rule is to ask three questions:

  1. Is this value required for the operation?
  2. Can the application safely continue without it?
  3. If we recover, will the user or developer know what happened when that matters?

Sometimes the right response is a default. Sometimes it’s a warning. Sometimes the program needs to stop and report an error.

The context determines which approach is appropriate.

6. The Null-Conditional Operator: A Convenient Shortcut

C# also provides the null-conditional operator (?.), which lets you access a member or call a method only when the reference on the left isn’t null.

For example, instead of writing:

if (customer != null)
{
    Console.WriteLine(customer.Name);
}

You can write:

Console.WriteLine(customer?.Name);

If customer is null, the expression evaluates to null rather than attempting to access the Name property. In this example, Console.WriteLine handles that null value without throwing a NullReferenceException.

One place I use this operator frequently is when invoking events and delegates.

Consider a C# event:

public event Action OnSomethingChanged;

When we want to notify any subscribers, we can write:

OnSomethingChanged?.Invoke();

This checks whether OnSomethingChanged is null before invoking it. If nobody has subscribed to the event, nothing happens. Without that check, calling OnSomethingChanged.Invoke() when the event has no subscribers would throw a NullReferenceException.

The same pattern works with a delegate variable:

Action callback = null;

callback?.Invoke();

In this example, the invocation is simply skipped because callback is null.

The null-conditional operator is convenient, but it doesn’t validate everything beyond the reference being checked. For example:

Console.WriteLine(customer?.Name.Length);

This safely handles a null customer reference, but if customer exists and Name is null, accessing Length can still throw an exception.

To protect against both possibilities, we can chain the null-conditional operator:

Console.WriteLine(customer?.Name?.Length);

Now, if either customer or Name is null, the expression evaluates to null.

As with other defensive programming techniques, the important thing is understanding exactly what you’re checking. The null-conditional operator makes code more concise, but it doesn’t eliminate the need to understand the data your application is working with.

7. Defensive Programming Shouldn’t Hide Bugs

There’s another trap worth mentioning: catching exceptions without doing anything useful.

try
{
    ProcessOrder(order);
}
catch (Exception)
{
    // Do nothing.
}

This code suppresses exceptions without resolving the problem or communicating that something went wrong.

The application might continue running, but the order could remain unprocessed. The absence of a visible crash doesn’t mean the operation succeeded.

A better approach is to handle exceptions when you can take meaningful action. That might mean logging useful information, notifying the caller, retrying a transient operation when safe, or presenting an appropriate error message.

Likewise, logging every null value as a warning isn’t necessarily helpful. If null is an expected state, a warning may create noise. If null indicates an impossible or dangerous state, a warning or exception may be justified.

The goal is to make failures understandable and recoverable where possible, not to make every error disappear.

8. A Note About Nullable Reference Types

Modern C# provides nullable reference types, which can help developers identify potential null-reference problems during development.

For example:

string name = "Tony";
string? optionalName = null;

With nullable reference types enabled, string indicates that the variable is intended to hold a non-null string, while string? indicates that null is an allowed value.

The compiler can warn about code that may dereference a potentially null value.

This feature is useful, but it isn’t a guarantee that null-reference exceptions are impossible. Nullable annotations primarily provide compile-time analysis. They don’t automatically validate every value arriving from external systems, older code, reflection, or other runtime sources.

You still need to understand your data and validate it at appropriate boundaries.

Conclusion

Null-reference problems often arise because we check one thing while assuming another is also valid.

An object reference can be non-null while one of its properties is null. A null check can protect against one failure without guaranteeing that the next operation is safe.

Defensive programming means understanding those distinctions and deciding how your application should respond to each invalid or unexpected state.

Use guard clauses to make assumptions explicit. Use defaults when a fallback is genuinely safe. Log meaningful problems, and don’t suppress exceptions simply to keep the application running.

Most importantly, validate the value you’re actually going to use.

That small habit can make a significant difference in the reliability and maintainability of your code.

Leave a Reply

Your email address will not be published. Required fields are marked *