Dealing with NullReferenceException

gfraiteur

Gael Fraiteur

Posted on May 22, 2024

Dealing with NullReferenceException

NullReferenceException

As a C# programmer, you've likely come across this exception: System.NullReferenceException: Object reference
not set to an instance of an object
. So, what does it mean? How can you deal with it? And better yet, how can you prevent it from happening?

What is a NullReferenceException?

With reference types (classes, interfaces, delegates, or arrays, but not structs), variables (i.e. local variables, fields, or parameters) do not contain the object itself but a reference to the object. This allows two different variables to reference the same object. A reference can either point to an object or be null, which means it doesn't point to any object.

NullReferenceException, as the name suggests, is thrown when you attempt to get the object referenced by a null reference. Specifically, it occurs when you try to access a non-static member (a property, method, field, or event) of a null reference.

Confusingly, C# also has reference parameters, variables, and return values, which are denoted by the ref keyword. This allows representing a reference to value types or... references to references! But since ref locations can't be null, you can't get a NullReferenceException from accessing them, and we won't cover them further in this article.

Another related concept is nullable value types, which are value types that can be null. These are denoted as T? (or Nullable<T>), where T is a value type (a struct or enum). You can't get a NullReferenceException by accessing a nullable value type because they're not references. Instead, accessing Value on a null nullable value type throws an InvalidOperationException.

How to identify the cause of a NullReferenceException?

Before you can fix a NullReferenceException, you first need to understand where it happened.

The simplest approach is to use the debugger of your IDE (like Visual Studio or Rider) to run your code. Consider this simple example:

class Shop(Cart cart)
{
     public void AddToCartWithDiscount(Product product, int discount)
     {
         product.Discount = discount;
         cart.Add(product);
     }
}
Enter fullscreen mode Exit fullscreen mode

When running code that calls the AddToCartWithDiscount method in the Visual Studio debugger, you might see something like this:

Visual Studio in debugger mode with NullReferenceException shown

Here, you can see where the exception occurred, the values of local variables, the call stack, and even which variable caused the exception by being null. If the null reference was passed as a parameter, as in this case, you can double-click a method in the call stack to see its state, getting closer to the root cause of the exception.

But what if you can't use a debugger? For instance, if you're working on a production issue and can't reproduce it locally. In that scenario, you should have access to the exception and its stack trace, which might look like this:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Shop.AddToCartWithDiscount(Product product, Int32 discount) in Shop.cs:line 5
   at Program.Main() in Program.cs:line 5
Enter fullscreen mode Exit fullscreen mode

There's less information here, but you can still see where the exception occurred and what the call stack is, which should suffice to figure out which value is null. However, keep in mind that the line numbers might not be accurate, especially if you're using a release build.

You could also add logging to your code to get more information about the state of your application when the exception happens.

If debugging information wasn't available when the exception occurred (it's usually contained in a .pdb file, but can also be embedded directly in your .NET executable), you won't even get line numbers. In that case, unraveling what's null will require more detective work, making logging even more valuable.

When tracing the source of the null value, remember that it might not be visible as null in the code. In C#, null is the default value for reference types, so it could also come from a field that was never assigned or an array element that was never set.

By convention, in well-written applications using well-written libraries, all instances of NullReferenceException must be blamed on the author of the code that throws this exception. However, in codebases that do not follow defensive programming practices, finding the root cause can be more challenging.

Before showing how you can implement defensive programming and make these exceptions easier to diagnose, we will see how to cope with null values when they are legitimate and expected.

How to fix a NullReferenceException?

Once you've identified what variable is null, you can address the issue.

There are essentially two different kinds of situations, each with a different solution:

  1. When null is a legitimate value for the variable or parameter because it is optional, and you need to cope with it. For instance, if in a class representing a tree node, the Parent property can legitimately contain a null reference for the root node.
  2. When null is not an expected value, but some external code sent it to you by mistake, you must defend your code against it. This technique is called defensive programming.

Let's look at the first approach first.

How to cope with legitimate null references?

When a reference can legitimately be null (likely because the value is optional), you must cope with it. In that case, you must check if the reference is null before accessing the member. The C# language provides various ways to do this, from a simple != null check in an if statement, through pattern matching with is and switch expressions, to null-related operators ?. and ??. Which one to use depends on the context and your (or your team's) preference.

Here are some examples:

// Simple null check.
if ( product != null )
{
    cart.Add( product );
}

// Pattern matching with an empty pattern checks for null 
// and assigns to a variable.
if ( shop.SpecialOffer is { } specialOffer )
{
    cart.SetDiscount( specialOffer );
}

// Pattern matching using negated null pattern.
if ( shop is { SpecialOffer: not null } )
{
    cart.SetDiscount( shop.SpecialOffer );
}

// Null-conditional and null-coalescing operators combined 
// to set a default value when the reference is null.
var actualDiscount = discount?.Percent ?? 0;
Enter fullscreen mode Exit fullscreen mode

How do I get rid of a NullReferenceException altogether?

Okay. You fixed a NullReferenceException. Now, you want to avoid them from happening in the future. How to do that? This is not a trivial question. Indeed, this problem has been called the billion-dollar mistake.

We suggest a four-step approach:

  1. Enable nullability analysis in your project and fix all warnings
  2. Adopt a contract-based mindset
  3. Check preconditions at the surface of your component
  4. Reduce boilerplate code and human errors

Step 1. Enable nullability analysis in your project and fix all warnings

You might wonder how you can know if a variable, parameter, or field can or cannot contain a null reference so that you can decide if you need to write the code to handle the null case.

To resolve this problem, the C# language now includes a feature called nullable reference types. It allows you, and the libraries you use, to specify which references can be null and which can't. This feature is optional but enabled by default in new projects targeting modern .NET. We suggest you enable this feature in all projects.

You can enable it by adding the following code to your csproj file or, better, to Directory.Build.props:

<PropertyGroup>
  <Nullable>enable</Nullable>
  <LangVersion>latest</LangVersion>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

To enable the latest C# language features with legacy target frameworks (such as .NET Framework 4), you can use PolySharp.

Now that nullable reference types are enabled, you mark a reference type as nullable by adding a ? to it. Conversely, if you don't mark a reference as nullable, it is considered to be non-nullable. Based on these annotations, the compiler or the IDE will report warnings in two cases:

  • when you try to access a member of a nullable reference without checking if it's null, and
  • when you try to assign null or a nullable reference to a non-nullable variable, field, or parameter.

For instance, the C# compiler reports a warning in the following code:

public decimal ComputePrice( Product product, 
                             decimal quantity, Discount? discount )
{
    // C# reports a warning on `discount.Percent` 
    // because `discount` can be null.
    return product.Price * quantity * (1 - discount.Percent / 100m);
}
Enter fullscreen mode Exit fullscreen mode

But the following code is correct:

public decimal ComputePrice( Product product, 
                             decimal quantity, Discount? discount )
{
    return product.Price * quantity * (1 - (discount?.Percent ?? 0) / 100m);
}
Enter fullscreen mode Exit fullscreen mode

Step 2. Adopt a contract-based mindset

A key mindset in programming is that of a contract. When you write a method, you are implicitly entering a contract with the caller of the method. This contract is "if you call me and you fulfill conditions A, B, and C, I will perform actions P, Q, R, and return Z".

Nullability annotations on a method can be seen as a contract between your method and its caller. If you mark a parameter as nullable, it means that you are willingly accepting null values. However, if you don't, it means that it is the caller's responsibility to provide you with a non-null value.

The same applies to the return value: unless you mark the return type as nullable, you are committed to returning a non-null value to the caller. If you return null, then your method, and not the caller, is to blame for the NullReferenceException it will cause.

So, who is to blame for the NullReferenceException in the following snippet? The author of GetLastOrder or RepeatLastOrder? Of course, RepeatLastOrder, because GetLastOrder clearly stated it could return null!

// This method requires a Customer but does not promise to return an Order.
public Order? GetLastOrder( Customer customer ) 
    => customer.Orders.OrderByDescending( o => o.Date ).FirstOrDefault();

public void RepeatLastOrder( Customer customer )
{
    var lastOrder = this.GetLastOrder( customer );

    // This will throw NullReferenceException 
    // if the customer never ordered.
    var newOrder = lastOrder.Clone( DateTime.Today );
    this.PostOrder( newOrder );
}
Enter fullscreen mode Exit fullscreen mode

For more complex nullability contracts, Microsoft also provides several nullability contract attributes. For example, consider the Dictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) method. For a non-nullable reference type TValue, its value parameter is null if the method returns false, but not-null if it returns true. This can't be expressed just by applying ?, which is why these attributes exist.

Step 3. Check preconditions at the surface of your component

To make it easier to diagnose NullReferenceException in large applications, a good practice is to implement a practice called defensive programming. Instead of accepting that your code could fail because of someone else's mistake (because this person could ignore warnings), defensive programming advocates that you should validate all inputs (specifically all preconditions) and throw a specific kind of exception if any precondition is not fulfilled.

There are three categories of exceptions in .NET:

  • Precondition failure exceptions are the ones that put the blame on the caller of the component:

    • ArgumentException: means that the caller sends an invalid parameter. For instance, throw NullArgumentException if the caller sends you a null that you don't expect.
    • InvalidOperationException: means that the operation is not available in the current state of the object. For instance, you are trying to write into a file opened in read-only mode.
    • NotSupportedException: means that the operation is never available for this kind of object.
  • Some must be blamed on the component itself: NullReferenceException, ArithmeticException, InvalidCastException, ...

  • Finally, other exceptions must be blamed on the user or the environment: OutOfMemoryException, IOException, OperationCanceledException, ...

Because any NullReferenceException in your code will be blamed on nobody else than you, it's a good practice, not only to cover your back but also to ease the process of code troubleshooting, to check that the sender does not send you a null reference by mistake. In this case, you must throw an ArgumentNullException.

Another advantage of ArgumentNullException is consistency: if a method mistakenly receives a null reference, it might throw a NullReferenceException only sometimes. Such buggy code might linger in your codebase for a long time, and the exception might be hard to reproduce. Conversely, if you throw an ArgumentNullException, the caller will immediately see the bug and will hopefully fix it promptly.

The C# language offers you three ways to throw ArgumentNullException:

1. Use an if statement

The most obvious approach is to add the following code at the top of your method:

if ( customer == null )
{
    throw new ArgumentNullException( nameof(customer) );
}
Enter fullscreen mode Exit fullscreen mode

2. Use the new ThrowIfNull

Alternatively, you can use the helper ThrowIfNull method:

ArgumentNullException.ThrowIfNull( customer );
Enter fullscreen mode Exit fullscreen mode

This version is shorter and simpler but provides the same information (the name of the parameter that was null is captured using caller argument expression). Plus, it can even be slightly more efficient because it moves the rarely-executed throw statement to a separately compiled method.

3. Use an inline expression check

A less readable but compact approach is to use add ?? throw the first time you are using a parameter.

public Order? GetLastOrder( Customer customer )
    => (customer ?? throw new ArgumentNullException( nameof(customer) ))
        .Orders
        .OrderByDescending( o => o.Date )
        .FirstOrDefault();
Enter fullscreen mode Exit fullscreen mode

Now, where exactly should you place these checks? Best practice is to do this at the surface area of your components. That means checking parameters of public methods and constructors, and the set value in public property setters. This way, you can focus on one component at a time, ensuring that null reference issues don't arise from interactions between components.

Step 4. Reduce boilerplate code and human errors

If you're thinking that checking parameters of all public methods sounds like tedious work, you're absolutely right. Not only is this work boring and time-consuming, but it's also error-prone. It's easy to forget to check for nullability in an entry point of your API, and the C# compiler won't remind you.

This redundant code can be avoided using a tool like Metalama. With the Metalama.Patterns.Contracts package, you can annotate a parameter with the [NotNull] attribute, and it will automatically generate the null check for you. The same attribute works on fields and properties as well. This is a slight improvement, but nothing too exciting.

public Order? GetLastOrder( [NotNull] Customer customer )
    => customer.Orders.OrderByDescending( o => o.Date ).FirstOrDefault();
Enter fullscreen mode Exit fullscreen mode

The significant improvement comes from the ability to apply [NotNull] to an entire project at once. You can do this using a fabric that calls the VerifyNotNullableDeclarations method:

internal class Fabric : ProjectFabric
{
    public override void AmendProject( IProjectAmender amender )
        => amender.Outbound.VerifyNotNullableDeclarations();
}
Enter fullscreen mode Exit fullscreen mode

This will add null checks to non-nullable reference types in all public members of your project. There are also ways to limit this to certain namespaces or types, or to expand it to include private members. If you need even more customization, all the relevant code is open source, so you can modify it to suit your exact needs.

Other questions

How to handle a NullReferenceException?

You should not! The only exceptions that should be handled are the one that stem from the environment, like I/O or networking exceptions. Any NullReferenceException must be considered as a bug and must be fixed. If the NullReferenceException is due to a bug in a third-party library, you should study if it can be worked around by sending different input to this library, or handle the exception to avoid making further damages in your application.

What is a first-chance exception?

Sometimes, you may get a message like a first-chance exception of type system NullReferenceException occurred in your application. A first-chance exception is an exception that has not been handled yet.

Summary

NullReferenceException is one of the most common exception types in C#. They can be avoided by adopting a few best practices:

  • Enable null reference types in your C# project and use nullability annotations.
  • Aim for zero warnings.
  • Implement defensive programming, i.e., validate your inputs and proactively throw an ArgumentNullException.

Defensive programming makes it much easier to diagnose the root cause of a NullReferenceException because it logically isolates components from each other, with each component being responsible for itself and applying a least-trust principle to other components. Although it's a best practice, it also requires writing a lot of redundant code. Tools like Metalama can help add precondition checks without boilerplate code, leveraging the benefits of precondition checking without its inconveniences.


If you liked this article, make sure to subscribe to the Timeless .NET Engineer newsletter

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
gfraiteur
Gael Fraiteur

Posted on May 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About