Handling Null Properly

Guard clauses, ArgumentNullException.ThrowIfNull and the null-conditional operators, plus why reaching for ! is usually a warning sign.

SyntaxArgumentNullException.ThrowIfNull(object? argument, string? paramName = null)

Annotations tell you where null can appear. Deciding what to do about it is a separate job, and there are only three honest answers: substitute something, leave early, or refuse the call.

A guard clause is the "leave early" form. Handle the bad case in the first few lines and return, so everything after it deals only with a value that is present. The normal path stays flat instead of being wrapped inside an if.

Refusing the call

When null means the caller made a mistake, stop. ArgumentNullException.ThrowIfNull(name), added in .NET 6, is one line that does it. To throw is to abandon the method immediately and report the problem back to the caller; exceptions get a stage of their own later. It is shorthand for the longhand if (name is null) throw new ArgumentNullException(nameof(name)); and the two behave the same. The compiler learns from it too: after the call, the value counts as not null.

Substituting

?. yields null instead of failing, ?? supplies a replacement, and ??= fills a variable only when it is null. C# 14 allows ?. on the left of an assignment as well, so basket?.ItemCount = 3; assigns only when basket is not null.

What trips people up

The ! operator, the null-forgiving operator, silences a warning and changes nothing else. It is a promise you made that the compiler cannot check. Reaching for it usually means a declaration is wrong, or a check belongs somewhere it is not.

Example

Example · csharp
Console.WriteLine(Greet("Ada"));
Console.WriteLine(Describe(null));

// A guard clause: deal with the bad case first and leave. Everything
// after it works with a value that is definitely present.
static string Describe(string? name)
{
    if (name is null)
    {
        return "(anonymous)";
    }

    return name.ToUpperInvariant();
}

// When null is a caller mistake rather than a normal case, stop.
// After ThrowIfNull the compiler treats name as not null, so there
// is no CS8602 warning on the line below it.
static string Greet(string? name)
{
    ArgumentNullException.ThrowIfNull(name);
    return $"Hello, {name}";
}

// Output:
// Hello, Ada
// (anonymous)

When to use it

  • Validate the arguments of a public method before doing any work.
  • Fall back to a default when a configuration value is missing.
  • Clear a CS8602 warning with the check the compiler is asking for.
  • Replace a two-line null check around a single property assignment.

More examples

The shorthand and the longhand it replaces

nameof(path) turns the parameter into the text "path" at compile time, so renaming the parameter cannot leave a stale message behind. ThrowIfNull gets the same text without you writing it, from the argument expression you passed. Use the short form in new code and recognise the long form, which fills older codebases and is still the only option when the check is more than a null test.

Example · csharp
Console.WriteLine(Modern("data.csv"));
Console.WriteLine(Longhand("data.csv"));

// .NET 6 added ArgumentNullException.ThrowIfNull.
static int Modern(string? path)
{
    ArgumentNullException.ThrowIfNull(path);
    return path.Length;
}

// What it is shorthand for. Both throw ArgumentNullException with
// ParamName "path", so you can use either and read both.
static int Longhand(string? path)
{
    if (path is null)
    {
        throw new ArgumentNullException(nameof(path));
    }

    return path.Length;
}

// Output:
// 8
// 8

Substituting a value, and the cost of !

The ?. and ?? pair is the whole substitution pattern: one operator stops the chain, the other decides what the result should be. The ! on the last block is the opposite: it produces no code at all and only moves responsibility from the compiler to you. Every ! in a codebase is worth a second look in review, because the common cause is a declaration that should have said string rather than string?.

Example · csharp
string?[] inputs = ["ada", "grace", null];

foreach (string? input in inputs)
{
    // ?. yields null instead of failing when input is null.
    // ?? then supplies the replacement. Together they cover both cases.
    string upper = input?.ToUpperInvariant() ?? "(missing)";
    Console.WriteLine(upper);
}

// ! only silences the compiler. It changes no behaviour, so if the
// value really were null the program would still fail here at run time.
string? known = "set";
int length = known!.Length;
Console.WriteLine(length);

// Output:
// ADA
// GRACE
// (missing)
// 3

Null-conditional assignment, new in C# 14

This removes a very common two-line if around a single assignment. Compound assignments such as += and -= work the same way; ++ and -- are not allowed. The detail worth remembering is that the right-hand side is skipped entirely when the left side is null, so a method call there never runs. Only use the shorthand where doing nothing is genuinely the right outcome, which is exactly when a guard clause or ThrowIfNull would be wrong.

Example · csharp
Basket? basket = new Basket();
Basket? missing = null;

// C# 14 allows ?. and ?[] on the left of an assignment. The right
// side runs only when the left side is not null.
basket?.ItemCount = 3;
missing?.ItemCount = 3;    // does nothing at all

// The longhand this replaces, and the only form earlier C# allowed.
if (basket is not null)
{
    basket.ItemCount = 4;
}

Console.WriteLine(basket?.ItemCount);
Console.WriteLine(missing?.ItemCount is null);

sealed class Basket
{
    public int ItemCount { get; set; }
}

// Output:
// 4
// True

Discussion

  • Be the first to comment on this lesson.