
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
requiredmodifier (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.ThrowIfNullfor 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 enableper file rather than all at once. - Generics and
T. An unconstrained genericTmay be a value type or a reference type, so the compiler is conservative. UseT?together with constraints likewhere T : classorwhere T : notnullto 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
requiredor 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.csprojor#nullable enableper 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.ThrowIfNulland therequiredkeyword 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.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment