Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
How C# 10.0 and .NET 6.0 improve ArgumentExceptions

C# methods often start by checking their arguments. A new language feature added in C# 10.0 has made it possible for .NET 6.0 to add some helpers that better support this, enabling you to simplify these checks, while also ruling out a common mistake.

Null argument checks before C# 10.0

C# developers are accustomed to seeing this sort of thing:

public bool DoSomething(string id, string name, string favouriteColour)
{
    if (id is null)
    {
        throw new ArgumentNullException(nameof(id));
    }
    if (name is null)
    {
        throw new ArgumentNullException(nameof(id));
    }
    if (favouriteColour is null)
    {
        throw new ArgumentNullException(nameof(favouriteColour));
    }

    // ... now go on to do something useful
}

Public methods often begin with a series of tests to detect unacceptable null inputs, or similar mistakes. Even if you use the Nullable Reference Types (NRT) features introduced in C# 8.0 to enlist the compiler's help in avoiding unwanted nulls, you still need checks like these because NRTs make no guarantees. (For example, the code calling one of your library functions might have been compiled with the NRT feature disabled. The .NET SDK leaves this feature disabled by default, and it was only with .NET 6.0 that .NET development tools commonly started enabling this in new projects.)

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

There are a couple of problems with this. First, it's verbose. Depending on your code style preferences, you might be able to mitigate that by moving the throw onto the same line as the if, and losing the braces, but it's still a bit of a mess.

The second problem is that it's easy to make a particular mistake. I've made that mistake here: did you spot it? My second throw passes the wrong argument name. So if somebody passes a null for name, I'll throw an exception erroneously complaining that they passed a null id. This issue is slightly less bad than it was a few years ago, thanks to nameof: at least if I rename an argument by refactoring it, this sort of reference to its name will automatically pick up the change (and if I rename it without using a refactoring, I'll get a compiler error where I used nameof with the old name). But that doesn't help me if I referred to entirely the wrong argument.

Null argument checks in C# 10.0 and .NET 6.0

Let's see how it looks in C# 10.0 on .NET 6.0:

public bool DoSomething(string id, string name, string favouriteColour)
{
    ArgumentNullException.ThrowIfNull(id);
    ArgumentNullException.ThrowIfNull(name);
    ArgumentNullException.ThrowIfNull(favouriteColour);

    // ... now go on to do something useful
}

There's no doubt that this is more compact. And I find that the resulting layout makes it much easier to see at a glance which arguments I'm dealing with here (although that's subjective, of course).

There's also no longer any opportunity to make the mistake my first example made with the name argument: I can't check one argument for null, and then throw an exception that accidentally points the finger at a completely different argument. This is impossible here because I only name each argument once.

But how does this work? How is ThrowIfNull going to discover the argument name? I've passed it the value of the argument, but it's not obvious how, at runtime, it's going to determine the name to use when throwing an exception. And yet it works exactly how you'd hope. How?

The CallerArgumentExpression attribute

The ThrowIfNull helper is able to discover the argument name thanks to a new feature in C# 10.0: support for the CallerArgumentExpression attribute. (This type has been in the .NET class library since .NET 5.0, but the compiler only started supporting it in C# 10.0. The ThrowIfNull helper also was added in .NET 6.0.) Here's how the declaration of ThrowIfNull looks:

public static void ThrowIfNull(
    [NotNull] object? argument,
    [CallerArgumentExpression("argument")] string? paramName = null)

This has a second, optional argument, annotated with the [CallerArgumentExpression] attribute. This attribute takes a single argument that is required to identify some other parameter (and the C# compiler will tell you if the name you've used doesn't match any of your parameter names). This works in a similar way to longer-established attributes such as [CallerMemberName] and [CallerLineNumber]: when a method has one of these attributes on an optional parameter, and when some code calls that method without supplying an explicit value for that parameter, the C# compiler generates code that supplies the relevant contextual information. With [CallerMemberName], it supplies the name of the method or property from which the call is being made. And with this new [CallerArgumentExpression] it provide the text of the expression that was used to supply the argument value. So in this case those first three lines effectively become this:

ArgumentNullException.ThrowIfNull(id, "id");
ArgumentNullException.ThrowIfNull(name, "name");
ArgumentNullException.ThrowIfNull(favouriteColour, "favouriteColour");

The [NotNull] on the first argument is not strictly on-topic, but worth discussion because its behaviour is slightly non-obvious. Why is the argument declared as nullable (object? rather than object) and yet marked as [NotNull]? This attribute expresses a post-condition: it says that if this method returns, then the compiler can safely deduce that argument was not null. I blogged about this in detail at https://endjin.com/blog/2020/06/dotnet-csharp-8-nullable-references-notnull but the essence is that any code following a call to ThrowIfNull will treat its input as being non-null even if the argument or variable in question was declared as nullable.

Other uses for CallerArgumentExpression

If you read the original design documentation for CallerArgumentExpression, it leads with a different use case: improved assertions. They show an example based on Debug.Assert:

Debug.Assert(array != null);
Debug.Assert(array.Length == 1);

The problem with this code, as they explain, is that at runtime you get no indication of which of these two assertions failed. The Debug.Assert method being used here just takes a bool, so at runtime all it can do is tell you that something failed; it can't tell you what failed.

The thinking was that [CallerArgumentExpression] would make it possible for the assertion to provide more useful information, because those calls could be turned into this:

// NOTE: doesn't actually work like this with the real Debug.Assert
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");
Programming C# 10 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

This underlines the fact that the compiler handles this attribute by passing the text of the expression that was used for the argument. With ThrowIfNull, the assumption is that this will be just the argument on its own. But here, the point is that it might be more complex.

In practice, Debug.Assert has not been updated to support this in .NET 6.0, mainly because there's no way to add it without it being a breaking change. (A new overload would not constitute a binary breaking change, but it would change the interpretation of most calls to Debug.Assert in existing code.)

But it seems likely that unit testing libraries and assertion libraries will adopt this to provide better reporting of test failures.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 10.0, and has written Pluralsight courses on WPF (and here) and the TPL. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects. Technology brings him joy.