100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Nullable Reference Types

See how C# 8's nullable reference types add compile-time null-safety annotations on top of the runtime's existing nullable value type system.

Async & Modern C#Intermediate8 min readJul 9, 2026
Analogies

Nullable Reference Types

Before C# 8, every reference type — string, class instances, arrays — could always be null, and the compiler gave no warning if you dereferenced one without checking, making NullReferenceException the most common runtime error in the language's history. Nullable reference types (NRT), enabled with #nullable enable or the project-wide <Nullable>enable</Nullable> setting, change this: reference types are treated as non-nullable by default, and you must explicitly opt in with a ? suffix (e.g. string?) to declare that a variable may hold null. This is a compile-time, flow-analysis feature only — it does not change runtime behavior or add actual null checks, but it lets the compiler warn you at the exact places your code's null-safety assumptions might be violated.

🏏

Cricket analogy: Before video review existed, an umpire's on-field call was final and no one flagged a potentially wrong decision in advance; NRT is like adding DRS that warns 'this call might be wrong' before the game continues.

Annotations and flow analysis

With NRT enabled, string means 'never null' and string? means 'may be null.' The compiler performs static flow analysis to track when a nullable value has been checked (e.g. inside if (value != null)) and treats it as non-null within that branch — this is called null-state tracking. Assigning null to a non-nullable reference type produces a warning (CS8600-series), as does dereferencing a ? type without a preceding null check (CS8602).

🏏

Cricket analogy: A fielder positioned at first slip is treated as 'definitely there' once the captain confirms the field placement, just as flow analysis treats a checked nullable value as guaranteed non-null within that branch.

csharp
#nullable enable

public record Customer(string Name, string? MiddleName, Address? BillingAddress);

public static class InvoiceFormatter
{
    public static string FormatRecipient(Customer customer)
    {
        // customer.Name is non-nullable — no check needed
        var displayName = customer.MiddleName is null
            ? customer.Name
            : $"{customer.Name} {customer.MiddleName}";

        // customer.BillingAddress is nullable — compiler flags a warning
        // if you dereference it without narrowing first
        string city = customer.BillingAddress?.City ?? "Unknown";

        return $"{displayName} — {city}";
    }

    public static Customer RequireCustomer(Customer? candidate)
    {
        ArgumentNullException.ThrowIfNull(candidate);
        return candidate; // narrowed to non-null after the guard
    }
}

Nullable value types vs nullable reference types

It's worth distinguishing the two nullable systems. Nullable<T> (the T? syntax for value types like int?) has existed since C# 2 and is a real runtime wrapper struct that actually can hold 'no value' — it's a genuine language feature backed by System.Nullable<T>. Nullable reference types, by contrast, are purely a compile-time annotation and analysis layer; string? and string compile to the exact same IL and the exact same runtime type. NRT is entirely about catching bugs earlier, not about changing what values a variable can physically hold.

🏏

Cricket analogy: A genuine no-ball called by the front-foot no-ball technology is a real, physical event affecting the game (like Nullable<T>), whereas a commentator's 'that might be close to a no-ball' remark is just advisory analysis (like NRT).

Migrating existing code

Enabling NRT on a large existing codebase typically surfaces hundreds of warnings at once. Common tools for managing this include the ! null-forgiving operator (tells the compiler 'trust me, this isn't null here' without a runtime check), attributes like [NotNullWhen], [MaybeNull], and [MemberNotNull] for describing more complex nullability contracts on APIs, and enabling the feature file-by-file or project-by-project rather than all at once.

🏏

Cricket analogy: Retrofitting a stadium with new safety regulations surfaces hundreds of code violations at once; officials use temporary waivers ('trust me, it's fine here') and section-by-section inspections rather than shutting the whole ground down.

Kotlin baked non-null-by-default types into its type system from day one, making String and String? genuinely distinct types enforced consistently everywhere. C#'s NRT is a compiler-only overlay added on top of an existing runtime type system for backward compatibility, so it can be silently defeated by the ! operator, by code compiled without NRT enabled, or by reflection — it's a strong linting tool, not an ironclad guarantee.

NRT warnings do not prevent your program from compiling or running by default, and they are only warnings, not errors, unless you configure <WarningsAsErrors> — teams that leave NRT warnings unaddressed get little practical benefit, since the whole point is catching potential NullReferenceExceptions before they happen at runtime.

  • Nullable reference types make reference types non-nullable by default when #nullable enable is active.
  • A ? suffix (e.g. string?) opts a reference type into being nullable.
  • NRT is purely compile-time flow analysis — it produces no IL differences and no runtime null checks.
  • This is distinct from Nullable<T> for value types, which is a real runtime wrapper struct.
  • The null-forgiving operator ! suppresses a specific warning without adding an actual safety check.
  • NRT warnings only help if treated seriously — consider enabling warnings-as-errors for nullability in CI.

Practice what you learned

Was this page helpful?

Topics covered

#CStudyNotes#Programming#NullableReferenceTypes#Nullable#Reference#Types#Annotations#StudyNotes#SkillVeris#ExamPrep