Gael Fraiteur
Posted on May 22, 2024
As a C# programmer, you've likely come across this exception: System.NullReferenceException: Object reference
. So, what does it mean? How can you deal with it? And better yet, how can you prevent it from happening?
not set to an instance of an object
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);
}
}
When running code that calls the AddToCartWithDiscount
method in the Visual Studio debugger, you might see something like this:
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
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:
- 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, theParent
property can legitimately contain anull
reference for the root node. - 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;
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:
- Enable nullability analysis in your project and fix all warnings
- Adopt a contract-based mindset
- Check preconditions at the surface of your component
- 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>
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);
}
But the following code is correct:
public decimal ComputePrice( Product product,
decimal quantity, Discount? discount )
{
return product.Price * quantity * (1 - (discount?.Percent ?? 0) / 100m);
}
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 );
}
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, throwNullArgumentException
if the caller sends you anull
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) );
}
2. Use the new ThrowIfNull
Alternatively, you can use the helper ThrowIfNull
method:
ArgumentNullException.ThrowIfNull( customer );
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();
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();
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();
}
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
Posted on May 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.