Table of Contents

  1. Introduction
  2. What a Null Guard Actually Is
  3. Why Guards Exist: The Fail-Fast Principle
  4. The Nullable Reference Types Twist
  5. The Boundary Rule: Where Guards Earn Their Keep
  6. What the Analyzers Say: CA1062 and CA2264
  7. Guards in Constructors
  8. Modern Tooling: ThrowIfNull and Friends
  9. Guard-Clause Libraries: Worth It?
  10. Performance Considerations
  11. A Decision Framework
  12. Conclusion

Introduction

Open almost any mature C# codebase and you will find methods that begin like this:

public Order CreateOrder(Customer customer, IReadOnlyList<LineItem> items)
{
    ArgumentNullException.ThrowIfNull(customer);
    ArgumentNullException.ThrowIfNull(items);

    // ... real work starts here
}

Those first two lines are guard clauses. They do nothing under normal operation and everything under abnormal operation. But in the era of nullable reference types (NRT), a fair question keeps coming up in code reviews:

“The compiler already forces callers to pass a non-null Customer. Isn’t this null check just noise?”

This post is an extensive, evidence-based answer to that question. The short version: null guards are not dead — but where you put them changed. The nuance is worth understanding, because getting it wrong means either drowning your code in ceremony or shipping libraries that fail with confusing NullReferenceExceptions deep in a call stack.

What a Null Guard Actually Is

A null guard is a small block of code at the top of a method or constructor that rejects invalid arguments before the method does any real work. For null arguments, the canonical behavior is to throw an ArgumentNullException.

The classic form:

public void Process(string input)
{
    if (input is null)
    {
        throw new ArgumentNullException(nameof(input));
    }

    // ...
}

Since .NET 6, there is a one-liner that does the same thing:

public void Process(string input)
{
    ArgumentNullException.ThrowIfNull(input);

    // ...
}

The key point is what kind of exception you throw. ArgumentNullException is a specific, actionable signal: it names the offending parameter and tells the caller “you gave me a null you were not allowed to give me.” That is fundamentally different from the NullReferenceException you get when a null slips past unguarded and is dereferenced ten frames deep. One points at the bug; the other points at a symptom.

Why Guards Exist: The Fail-Fast Principle

Guards are an application of fail-fast design. The idea is simple: when a component detects that something is wrong, it should stop immediately and loudly, rather than continuing on corrupted assumptions.

Consider what happens without a guard:

public decimal CalculateDiscount(Customer customer)
{
    // customer is null, but we don't check
    return customer.LoyaltyTier switch      // 💥 NullReferenceException here
    {
        LoyaltyTier.Gold => 0.15m,
        LoyaltyTier.Silver => 0.10m,
        _ => 0m,
    };
}

The NullReferenceException that results is:

  • Ambiguouswhich reference was null? In a longer method with several objects, you cannot tell from the exception alone.
  • Delayed — the failure surfaces at the point of use, which may be far from the point where the bad value entered the system.
  • Uninformative to the caller — nothing in the message names the parameter that was wrong.

A guard converts all three problems into a single, precise message:

S y s t e m . A r g u m e n t N u l l E x c e p t i o n : V a l u e c a n n o t b e n u l l . ( P a r a m e t e r ' c u s t o m e r ' )

That is the entire value proposition. Guards trade a few lines of code for diagnosability. The question of whether you need them is really the question of whether that trade is worth making at a given location — and that depends heavily on who can call the method.

The Nullable Reference Types Twist

C# 8 introduced nullable reference types. With <Nullable>enable</Nullable> in your project, the compiler tracks nullability and warns you when you might pass or dereference a null incorrectly. So a natural conclusion is:

“If customer is typed as non-nullable Customer (not Customer?), the compiler guarantees it’s never null. Guards are redundant.”

This conclusion is wrong, and understanding why is the crux of the whole topic.

Nullable reference types are a compile-time, advisory feature. They are not enforced at runtime. Specifically:

  1. The annotations are erased. NRT information exists as metadata and compiler analysis. There is no runtime barrier that stops a null from occupying a non-nullable reference. A non-nullable string parameter can absolutely hold null at runtime.

  2. Callers can ignore or suppress the warnings. Any caller can use the null-forgiving operator (customer!), disable nullable context in their own file, or simply build with warnings-not-as-errors. The “guarantee” evaporates the moment someone opts out.

  3. Callers may not use NRT at all. If you ship a library, your consumers might be on an older language version, a project with <Nullable>disable</Nullable>, or a different language entirely (F#, VB, or a dynamic caller via reflection). To them, your non-nullable annotation is invisible.

  4. Nulls arrive from places the compiler can’t see. Deserialization (JSON, XML), reflection, default(T) in generics, interop, ORMs materializing rows, and data-binding frameworks all routinely produce nulls that the type system never had a chance to check.

So the accurate mental model is:

Nullable reference types reduce the probability that a null reaches your method, and move a whole class of bugs from runtime to compile time. They do not make it impossible. A guard is your runtime safety net for the cases the compiler could not cover.

This is exactly why the .NET team kept and even improved the guard tooling (ArgumentNullException.ThrowIfNull, shipped in .NET 6) years after NRT arrived in C# 8. If NRT had made guards obsolete, they would not have invested in making guards more ergonomic.

The Boundary Rule: Where Guards Earn Their Keep

If guards aren’t obsolete but you also don’t want to check every parameter in every method, where’s the line? The answer that most experienced teams converge on — and that Microsoft’s own guidance encodes — is the trust boundary:

Validate at the boundaries of your trust domain. Trust internal calls.

Concretely, ask: can code I don’t control call this method?

Public and protected members of a library — guard

These form the public API surface. You have no control over who calls them or what they pass. This is precisely where a null can arrive from an untrusted, unpredictable source, and precisely where a clear ArgumentNullException (rather than a mysterious NullReferenceException) pays off. Microsoft’s CA1062 rule exists specifically to enforce this:

“If a method can be called from an unknown assembly because it is declared public or protected, you should validate all parameters of the method.”

internal and private members — usually skip

If a method can only be called by code you wrote and compiled together, the null-checking burden shifts to the compiler. With NRT enabled, your own code cannot pass a null to a non-nullable internal parameter without a warning you’d see and fix. Guarding these is mostly ceremony. Microsoft’s own advice reflects this: if a method is designed to be called only by known assemblies, mark it internal and, if needed, use InternalsVisibleToAttribute — the analyzer will then stop demanding guards.

The pragmatic middle ground

  • Public API of a reusable library / NuGet package → guard aggressively. Your reputation is a stack trace that points at the caller’s mistake, not your internals.
  • Application code (a web app, a service you deploy as a unit) → guard at genuine entry points (controller actions, message handlers, deserialization boundaries) and trust the interior. NRT covers most of the rest.
  • Hot internal paths called millions of times → lean on NRT and skip the guard.

This boundary framing is what turns “always guard” and “never guard” — both wrong — into a rule you can actually apply.

What the Analyzers Say: CA1062 and CA2264

.NET ships two code-analysis rules that bracket this topic nicely.

CA1062 — “Validate arguments of public methods”

CA1062 fires when “an externally visible method dereferences one of its reference arguments without verifying whether that argument is null.” It is the analyzer embodiment of the boundary rule above. Notable details:

  • It is not enabled by default in current .NET SDK analysis, so many teams opt into it explicitly when shipping libraries.
  • It is highly configurable. You can scope it by api_surface (e.g. only public), exclude specific types, exclude the this parameter of extension methods, and — importantly — declare your own null-check validation methods so a shared helper satisfies the analyzer:
# .editorconfig
dotnet_code_quality.CA1062.null_check_validation_methods = ThrowIfNull|Guard.AgainstNull

There has been long-running discussion (see roslyn-analyzers issues #7214 and #2875) about CA1062 not fully accounting for NRT annotations — another reminder that the ecosystem still treats runtime null validation as a separate concern from compile-time nullability.

CA2264 — don’t guard something that can’t be null

The flip side, enabled by default as a warning since .NET 9: CA2264 flags passing a non-nullable value type or a known-non-null expression to ArgumentNullException.ThrowIfNull, because the check is a no-op that can never throw:

public void Save(int id)         // int is a value type — never null
{
    ArgumentNullException.ThrowIfNull(id);   // ⚠️ CA2264: this can never throw
}

Together, the two rules draw the correct picture: guard the references that can actually be null at a trust boundary, and don’t guard things that can’t.

Guards in Constructors

Constructors deserve special attention because they establish an object’s invariants — the conditions that must hold for the object’s entire lifetime. A guard in a constructor isn’t just about failing fast on one call; it protects every subsequent method from having to re-check the same field.

public sealed class OrderService
{
    private readonly IPaymentGateway _gateway;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IPaymentGateway gateway, ILogger<OrderService> logger)
    {
        _gateway = gateway ?? throw new ArgumentNullException(nameof(gateway));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    // Every method below can now assume _gateway and _logger are non-null,
    // and never has to check again.
}

Here the guard buys something extra: a single validated point of entry. Without it, a null dependency injected by a misconfigured container surfaces later as a NullReferenceException inside some business method, with a stack trace that says nothing about construction being the real fault. With it, the object refuses to exist in an invalid state — and the exception names the bad dependency.

Two practical notes:

  • The x ?? throw form is idiomatic and lets you assign and validate in one expression. ArgumentNullException.ThrowIfNull(gateway) on a preceding line is equally fine and reads a bit cleaner when there’s no assignment shortcut.
  • Copy constructors are a known CA1062 trap: public Person(Person other) : this(other.Name, other.Age) dereferences other before the body runs, so it needs a static pass-through null check to be safe (Microsoft documents this exact pattern).

Because dependency-injection containers, deserializers, and reflection all construct objects in ways the compiler can’t fully police, constructors of public/injected types are one of the highest-value places to keep guards — even with NRT on.

Modern Tooling: ThrowIfNull and Friends

If you decide a guard is warranted, use the modern API. Since .NET 6, ArgumentNullException exposes a static helper:

public static void ThrowIfNull(object? argument, string? paramName = default);

The magic is the paramName parameter. It’s decorated with [CallerArgumentExpression], so the compiler fills in the text of the expression you passed automatically. You write:

ArgumentNullException.ThrowIfNull(customer);

and the runtime produces Value cannot be null. (Parameter 'customer') — no nameof, no repetition, no chance of the parameter name drifting out of sync with the code. Microsoft explicitly recommends you do not pass paramName yourself and let the caller-expression mechanism supply it.

The family grew over subsequent releases:

Helper Introduced Checks for
ArgumentNullException.ThrowIfNull .NET 6 null
ArgumentException.ThrowIfNullOrEmpty .NET 7 null or ""
ArgumentException.ThrowIfNullOrWhiteSpace .NET 8 null, "", or whitespace
ArgumentOutOfRangeException.ThrowIf... .NET 8 range/comparison checks

So a string parameter that must be a real, meaningful value becomes:

public User Register(string email)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(email);
    // email is now guaranteed non-null, non-empty, non-whitespace
}

Beyond terseness, there’s a subtle but real benefit: because the throw lives inside a separate, non-inlined helper method, the JIT can inline the happy path of your method more aggressively than it could with an inline if (x is null) throw ... block. You get cleaner code and, in hot paths, potentially better codegen.

Guard-Clause Libraries: Worth It?

A cottage industry of guard libraries exists — Ardalis.GuardClauses, Dawn.Guard, and others — offering fluent syntax:

// Ardalis.GuardClauses
Guard.Against.Null(customer);
Guard.Against.NullOrEmpty(items);
Guard.Against.OutOfRange(quantity, nameof(quantity), 1, 100);

They’re pleasant and expressive, especially when you need many kinds of validation (ranges, regex, enum-defined, negative numbers) in one place. But weigh the trade-off honestly:

  • For plain null checks, the built-in ArgumentNullException.ThrowIfNull now covers the common case with zero dependencies. Adding a package just for Guard.Against.Null is hard to justify in 2026.
  • For rich validation vocabularies across a large codebase, a library can standardize behavior and reduce hand-rolled variety — genuine value.
  • Remember to register the library’s guard methods as null_check_validation_methods in .editorconfig so CA1062 recognizes them.

The honest recommendation: start with the BCL helpers; reach for a library only when your validation needs clearly outgrow “is it null?”

Performance Considerations

A frequent objection to guards is cost. In practice, for the overwhelming majority of code, the cost is negligible and should not drive the decision:

  • A null comparison is a single, extremely cheap CPU operation.
  • ThrowIfNull is designed to be inlining-friendly: the check is tiny and the actual throw is factored into a separate method, so the common (non-null) path stays lean.
  • The ArgumentNullException allocation only happens on the failure path — which, by definition, is the path where you’ve already lost the performance game because something is broken.

The one place to be deliberate is extremely hot, tight internal loops called millions of times per second, where even a predictable branch matters and where the arguments are already validated upstream. There, skipping a redundant internal guard (and trusting NRT + your own boundary validation) is reasonable. This is a micro-optimization for a tiny fraction of code — not a general policy. “Guards are slow” is not a valid reason to omit them from a public API.

A Decision Framework

Putting it all together, here’s a checklist you can apply without agonizing over each method:

Situation Guard? Why
public / protected member of a library Yes Untrusted callers; CA1062; clear exception beats mystery NRE
Constructor of an injected/public type Yes Protects invariants; DI/reflection bypass NRT
App entry points (controllers, handlers, deserialization) Yes Data crosses a trust boundary from outside
internal / private helper in an NRT-enabled project Usually no Compiler already guards your own callers
Value-type parameter (int, Guid, struct) No Can’t be null; CA2264 will flag it
Extremely hot internal loop, args validated upstream No Redundant; micro-optimize
Parameter already validated by a preceding call No Don’t double-check; suppress CA1062 if needed

And three rules of thumb:

  1. Validate at boundaries, trust the interior. The boundary is “can code I don’t control reach here?”
  2. NRT reduces the need; it does not remove it. Annotations are erased at runtime and callers can opt out.
  3. When you do guard, use ArgumentNullException.ThrowIfNull and let [CallerArgumentExpression] name the parameter for you.

Conclusion

So — do you still need ArgumentNullException guards? Yes, but fewer of them, in the right places.

Nullable reference types are one of the best things to happen to C#. They catch a huge class of null bugs at compile time and legitimately let you delete guards from your internal, self-called code. But they are a compile-time convenience, not a runtime contract. The instant a value crosses a boundary you don’t control — a library’s public API, a DI container, a JSON payload, a reflection call — the type system’s guarantees no longer hold, and a runtime guard is the difference between an exception that names the culprit and one that buries it.

The modern answer isn’t “always guard” or “never guard.” It’s: guard your trust boundaries with ThrowIfNull, trust your interior to the compiler, and don’t guard what can’t be null. Do that, and your null checks stop being ceremony and start being exactly what they were always meant to be — a precise, fail-fast signal that points straight at the mistake.


References