Skip to main content

C# Nullable Reference Types: Stop Null Reference Exceptions

Learn C# nullable reference types to eliminate null reference exceptions for good. Practical examples, best practices, and pitfalls. Start writing safer C# today.

If you have written C# for any length of time, you have met the most infamous runtime error in the .NET ecosystem: the dreaded System.NullReferenceException. Sir Tony Hoare, who introduced the null reference in 1965, famously called it his "billion-dollar mistake." The good news is that modern C# gives you a powerful tool to wipe out this class of bug at compile time. In this tutorial you will learn exactly how C# nullable reference types work, how to enable them, and how to use them to eliminate null reference exceptions for good.

This guide is written for everyone: beginners searching for how to avoid null in C#, intermediate developers looking for nullable reference types best practices, and senior engineers who want to master advanced nullable annotation techniques. Every example below is runnable on .NET 6, 7, 8, or 9.

What Are C# Nullable Reference Types?

Before C# 8.0, every reference type (string, custom classes, arrays, and so on) could hold null by default, and the compiler said nothing about it. That silence is precisely why a null reference exception in C# is so common — the danger is invisible until your code blows up at runtime, often in production.

C# nullable reference types flip this model. When the feature is enabled, the compiler treats reference types as non-nullable by default. If you want a variable to legitimately hold null, you must opt in explicitly with a ? suffix, exactly like nullable value types (int?). The compiler then performs static flow analysis and warns you whenever you might dereference a null value.

It is critical to understand one thing up front: this is a compile-time feature, not a runtime one. Nullable reference types do not change the generated IL or add runtime checks. They are a set of annotations and warnings that help you catch mistakes before you ship. That makes them essentially free in terms of performance.

Nullable vs Non-Nullable: A Quick Mental Model

  • string name — non-nullable. The compiler expects this to never be null.
  • string? name — nullable. This may be null, and the compiler forces you to check before using it.

How to Enable Nullable Reference Types in C#

There are two ways to turn the feature on. The recommended approach for any new project is to enable it for the entire assembly in your .csproj file:

<PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <!-- Optional: treat nullable warnings as errors to enforce it -->
    <WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

Projects created with the modern .NET templates already include <Nullable>enable</Nullable>. If you are migrating a large legacy codebase, enabling it everywhere at once can produce thousands of warnings. In that case, turn it on file by file using a preprocessor directive at the top of each file:

#nullable enable

public class Customer
{
    public string Name { get; set; }   // non-nullable
    public string? MiddleName { get; set; } // explicitly nullable
}

The #nullable directive supports enable, disable, and restore, giving you fine-grained control during an incremental migration.

Seeing the Compiler Catch Bugs for You

Let us look at the feature in action. Consider this classic bug that would normally throw a NullReferenceException at runtime:

#nullable enable

public class UserService
{
    public string GetUpperName(string? input)
    {
        // CS8602: Dereference of a possibly null reference.
        return input.ToUpper();
    }
}

Because input is declared as string?, the compiler immediately emits warning CS8602. It is telling you, before you even run the program, that input could be null. To fix it, you simply add a null check, and the compiler's flow analysis is smart enough to understand it:

public string GetUpperName(string? input)
{
    if (input is null)
    {
        return string.Empty;
    }

    // No warning here — the compiler knows input is non-null.
    return input.ToUpper();
}

This is the heart of why nullable reference types eliminate null reference exceptions: the compiler refuses to let you ignore the possibility of null. You either handle it or explicitly tell the compiler you know better.

The Null-Handling Operators You Need to Know

Modern C# ships with a family of operators that make working with nullability concise and readable. Mastering these is a core part of nullable reference types best practices.

The Null-Conditional Operator (?.)

The ?. operator short-circuits and returns null instead of throwing if the left-hand side is null:

Customer? customer = GetCustomer();

// Returns null if customer is null, instead of throwing.
int? nameLength = customer?.Name.Length;

The Null-Coalescing Operators (?? and ??=)

string? maybeName = GetName();

// Provide a fallback when the value is null.
string safeName = maybeName ?? "Unknown";

// Assign only if the current value is null.
maybeName ??= "Default";

The Null-Forgiving Operator (!)

Sometimes you know more than the compiler. The null-forgiving operator ! tells the compiler "trust me, this is not null here." Use it sparingly — it is an escape hatch, and overusing it defeats the entire purpose of the feature:

// You have already validated this elsewhere.
string name = customer!.Name;

Pitfall: A misused ! can reintroduce exactly the null reference exception you were trying to avoid. Treat every ! as a small piece of technical debt that needs justification.

Guard Clauses and Modern Null Validation

For public APIs and library code, you should still validate arguments at runtime, because callers may have nullable warnings disabled or may be calling from code that ignores them. .NET 6 introduced a clean helper for this:

public void ProcessOrder(Order order)
{
    // Throws ArgumentNullException with the parameter name if null.
    ArgumentNullException.ThrowIfNull(order);

    // Safe to use order here.
    Console.WriteLine(order.Id);
}

This combines the best of both worlds: compile-time warnings for your own code and a clear, fail-fast runtime exception with the correct parameter name for everyone else.

Nullable Reference Types Best Practices

Now that you understand the mechanics, here are the practices that separate clean, null-safe codebases from frustrating ones.

  • Enable it project-wide on new code. Always start with <Nullable>enable</Nullable>. It is far easier to stay null-safe than to retrofit safety later.
  • Make non-null the default, nullable the exception. Only add ? when null is a genuine, meaningful state — for example, an optional middle name or a not-yet-loaded value.
  • Initialize non-nullable fields. Use constructors, the required modifier (C# 11+), or property initializers so non-nullable members are never left unassigned.
  • Avoid the null-forgiving operator (!) as a habit. Each use should have a comment explaining why null is impossible there.
  • Use guard clauses on public boundaries. Combine compile-time annotations with ArgumentNullException.ThrowIfNull for defense in depth.
  • Treat nullable warnings as errors in CI to prevent regressions from slipping in over time.

Handling the "required" Keyword (C# 11+)

A common warning when you enable nullability is CS8618: "Non-nullable property must contain a non-null value when exiting constructor." The required modifier solves this elegantly by forcing callers to set the property:

public class Product
{
    public required string Sku { get; set; }
    public required string Title { get; set; }
    public string? Description { get; set; } // genuinely optional
}

// The compiler enforces that Sku and Title are set.
var product = new Product { Sku = "ABC-123", Title = "Wireless Mouse" };

Common Pitfalls When Migrating Legacy Code

Turning on nullable reference types in an existing project surfaces a few recurring issues. Knowing them ahead of time saves hours.

  • The warning flood. A large codebase can produce thousands of warnings instantly. Migrate incrementally with #nullable enable per file rather than all at once.
  • Generics and T. An unconstrained generic T may be a value type or a reference type, so the compiler is conservative. Use T? together with constraints like where T : class or where T : notnull to clarify intent.
  • External libraries. Dependencies that have not been annotated are "null-oblivious." The compiler cannot reason about them, so wrap their outputs in your own null checks at the boundary.
  • Serialization and ORMs. Frameworks like Entity Framework Core and System.Text.Json set properties via reflection after construction, which can bypass your initialization. Use required or assign sensible defaults to keep the compiler happy.

Why This Matters: The WHY Behind the Feature

It is worth pausing on why Microsoft invested so heavily in this feature. Null reference exceptions are not just annoying — they are expensive. They typically surface at runtime, frequently in edge cases that escaped testing, and often in production where the cost of a crash is highest. Studies of large codebases consistently rank null dereferences among the most common categories of bugs.

By shifting the detection of these bugs left — from runtime all the way back to compile time — nullable reference types turn a whole class of expensive, hard-to-reproduce production failures into immediate, visible feedback in your editor. You fix the bug before it ever runs. That is a fundamental improvement in how reliable your C# applications can be, and it costs you nothing at runtime.

Conclusion and Key Takeaways

C# nullable reference types are one of the most impactful additions to the language for writing robust, maintainable code. By making the compiler your ally, you can finally eliminate null reference exceptions before they ever reach your users.

  • Nullable reference types are a compile-time feature with zero runtime cost.
  • Enable them with <Nullable>enable</Nullable> in your .csproj or #nullable enable per file.
  • Reference types become non-nullable by default; opt into null with the ? suffix.
  • Use ?., ??, and ??= for clean null handling, and reserve ! for rare, well-justified cases.
  • Combine compile-time annotations with ArgumentNullException.ThrowIfNull and the required keyword for defense in depth.
  • Migrate legacy code incrementally and treat nullable warnings as errors in CI.

Start by enabling nullable reference types on your next C# project today, fix the warnings the compiler shows you, and watch the NullReferenceException stack traces disappear from your logs for good. Your future self — and your on-call rotation — will thank you.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...