Featured image of post Compile-time null safety: How to avoid NullReferenceException in C#

Compile-time null safety: How to avoid NullReferenceException in C#

Catching null errors at compile time

Follow me

Introduction Link to this section

Softwares can fail at two different moments: compile time and runtime. In the context of errors, null is a problem because the effects of not handling them right are perceived only at runtime.

In this post, I’ll show how to use a C# language feature to move the null errors to compile time and help us avoid them at runtime.

The problem with null Link to this section

Null is a value that is not a value. It is used by reference types to indicate that the variable is not pointing to any value.

This creates a special case that needs to be checked every time the variable is accessed or it will throw an exception at runtime when the value is null.

This code, for example, will compile normally:

1
2
3
string nullHere = null;

Console.WriteLine($"Length: {nullHere.Length}"); //Trying to access Length of null

But will throw a NullReferenceException at runtime:

Unhandled exception. System.NullReferenceException:
Object reference not set to an instance of an object.

The solution is to check for null before accessing any reference type property or method:

1
2
3
4
5
6
string nullHere = null;

if (nullHere != null)
{
    Console.WriteLine($"Length: {nullHere.Length}");
}

The problem is remembering to check for null every time an object can be null. That’s where Nullable reference types comes to the rescue.

Nullable Reference Types Link to this section

Reference types, as the name implies, store references (pointers) to their data.

C# provides the following reference types:

  • string
  • object
  • class
  • record
  • interface
  • delegate
  • dynamic

All those types can have null assigned to them, making them a risk for a NullReferenceException.

C# 8 introduced Nullable Reference Types. It is a way to say that variables may contain null and get warnings every time their members are accessed without checking for nulls.

To declare nullable reference types, we use ? after the type name:

string? stringThatMayBeNull = null; //Nullable string

Person? personThatMayBeNull = null; //Nullable Person

List<Person>? personThatMayBeNull = null; //Nullable List of Non-Nullable Person

List<Person?> personThatMayBeNull = new(){ null }; //Non-Nullable List of Nullable Person

To enable nullable reference types, just add the following property to your projects:

<Nullable>enable</Nullable>

Nullable warnings Link to this section

When we enable nullable reference types in our projects, we start to get warnings when assigning null to non-nullable types:

Warning	CS8600	Converting null literal or possible null value to non-nullable type.

We also start to receive warnings when accessing a nullable type member when it can be null:

Warning	CS8602	Dereference of a possibly null reference.

The compiler knows when a variable isn’t null.

For instance, when we check for null in an if statement:

Compiler showing variable is not null inside an if check

And when we assign a non-nullable value to a nullable variable:

Compiler showing variable is not null when non-nullable value assigned

Treating nulls Link to this section

Besides checking for nulls with if statements, we can use null operators that C# provides to make the code cleaner.

Null conditional operator Link to this section

The null conditional operator (?.) allows us to access a reference type member only if its value is not null.

1
2
3
string? nullHere = null;

Console.WriteLine($"Length: {nullHere?.Length}"); //No NullReferenceException here

Null forgiving operator Link to this section

The null forgiving operator (!.) informs the compiler that we are sure that a nullable type variable that may be null is not null.

This is commonly useful when writing automated tests because you know the results that will be returned.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface INullSampleUseCase
{
    Person? GetPossibleNullPerson();
}

public class MyClass
{
    private readonly INullSampleUseCase _nullSampleUseCase;

    public MyClass(INullSampleUseCase nullSampleUseCase)
    {
        _nullSampleUseCase = nullSampleUseCase;
    }

    public Person? GetThePerson()
    {
        return _nullSampleUseCase.GetPossibleNullPerson();
    }
}

[Fact]
public void GetThePerson_ShouldGetRightPerson()
{
    var nullSampleUseCaseMock = new Mock<INullSampleUseCase>();

    nullSampleUseCaseMock
        .Setup(m => m.GetPossibleNullPerson())
        .Returns(randomPerson);

    var myClass = new MyClass(nullSampleUseCaseMock.Object);

    var personReturned = myClass.GetThePerson();

    Assert.Equal(randomPerson.FirstName, personReturned!.FirstName); //I'm sure personReturned won't be null here!
}

Null coalescing operator Link to this section

The coalescing operator (??) is used to assign the value of the right operand if the value of the left operand is null. It’s used to assign a nullable reference type to a non-nullable reference type, falling back to a default value in case of null.

1
2
3
4
5
string? maybeNullHere = getSomeValueOrNull();

string notNullHere = maybeNullHere ?? "Default value"; //Use "Default value" if maybeNullHere is null

Console.WriteLine($"Length: {notNullHere.Length}");

Enforcing null checks at build time Link to this section

To enforce null checks everywhere, we can increase the severity of the warnings to Error. This way, it won’t be possible to compile the projects without properly checking for nulls.

Unfortunately, compiler messages for nullables are not in code quality and code style categories explained in my previous post and can’t be set to errors unless done by message code:

1
2
3
4
dotnet_diagnostic.CS8602.severity = error
dotnet_diagnostic.CS8670.severity = error
dotnet_diagnostic.CS8601.severity = error
...

To do it, set the property TreatWarningsAsErrors to Nullable on all projects:

<TreatWarningsAsErrors>Nullable</TreatWarningsAsErrors>

After this, all warnings from nullability messages will have their severity equal to Error:

Error	CS8602	Dereference of a possibly null reference.

💡 We can set TreatWarningsAsErrors to true to treat all warnings as errors.

Required properties Link to this section

With nullable types enabled, we will receive the following errors on classes/records that have non-nullable reference types not initialized in the constructor or with default values:

CS8618	Non-nullable property ‘FirstName’ must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

C# 11 introduced the required modifier. It allows us to have non-nullable reference types without initializing them in the constructor by forcing us to specify the values for these properties when instantiating an object.

First we add the required modifier to the properties:

Required modifier

Then, when instantiating the object, we need to pass the values for the required properties:

Instantiating a class with properties values

If we don’t specify the values, the compiler will warn us:

Error	CS9035	Required member ‘Person.FirstName’ must be set in the object initializer or attribute constructor.

Automated Tests Link to this section

It’s worth noting that using the null conditional and the null coalescing operator will influence the code coverage of the project. They will be treated as branches and have to have tests for both scenarios.

For example, if we have this silly class:

1
2
3
4
5
6
7
public class NullSampleUseCase
{
    public string GetFormatedStringLength(string? maybeNullHere)
    {
        return $"Length: {maybeNullHere?.Length}";
    }
}

And this test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class NullSampleUseCaseTests
{
    [Fact]
    public void WhenNotNull_ShouldPrintLength()
    {
        var nullSampleUseCase = new NullSampleUseCase();
        var formatedLength = nullSampleUseCase.GetFormatedStringLength("My string");

        Assert.Equal($"Length: 9", formatedLength);
    }
}

The code coverage will be only 50% because the test is not covering the scenario receiving null.

Method partially covered

Creating a new test for the null scenario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class NullSampleUseCaseTests
{
    [Fact]
    public void WhenNotNull_ShouldPrintLength()
    {
        var nullSampleUseCase = new NullSampleUseCase();
        var formatedLength = nullSampleUseCase.GetFormatedStringLength("My string");

        Assert.Equal($"Length: 9", formatedLength);
    }

    [Fact]
    public void WhenNull_ShouldNotPrintLength()
    {
        var nullSampleUseCase = new NullSampleUseCase();
        var formatedLength = nullSampleUseCase.GetFormatedStringLength(null);

        Assert.Equal($"Length: ", formatedLength);
    }
}

Code coverage will be 100% for this class:

Method fully covered

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy