C# Nullable Reference Type: Enabling and Using It Safely
c# nullable reference type: Learn how to enable and use C# nullable reference types, understand compiler warnings, and migrate existing codebases safely.
The c# nullable reference type feature changes how the compiler treats reference types by separating the concept of "this type can be null" from "this type should never be null." When enabled, the compiler performs static analysis to warn you when a reference type that should not be null might receive a null value, and when a nullable reference type is dereferenced without a null check.
Enabling the Nullable Context
The nullable context can be enabled at the project level or per-file. At the project level, add the following to your .csproj:
<PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup>
This applies the nullable context to every C# file in the project. You can also enable it per-file with the #nullable enable directive, which is useful when migrating a large codebase incrementally:
#nullable enable public class CustomerService { public Customer? FindCustomer(int id) { /* ... */ } }
The Nullable project property accepts three values: enable, disable, and warnings. The warnings value turns on the compiler's nullability analysis without treating annotations as part of the public API contract. Most new projects should use enable; the other values exist for gradual adoption scenarios.
Annotating Reference Types with ?
Once the nullable context is enabled, every reference type in your code is considered non-nullable by default. A string parameter means "this must not be null." To express that a value may be null, append ? to the type:
public string? GetDisplayName(User user) { return user.Nickname ?? user.FullName; }
The compiler tracks the null state of each variable through the method body. If you dereference a string? without checking for null first, you get a warning:
string? name = GetDisplayName(user); int length = name.Length; // CS8602: Dereference of a possibly null reference
The fix is to check for null before dereferencing:
string? name = GetDisplayName(user); int length = name?..Length ?? 0;
This annotation system does not change runtime behavior. A string? and a string are the same System.String type at runtime. The ? is purely a compile-time contract that tells the compiler and other developers what your code expects.
The Null-Forgiving Operator !
Sometimes you know a value is not null even though the compiler cannot prove it. The null-forgiving operator suppresses the warning:
string? maybeNull = GetValue(); string definitelyNotNull = maybeNull!;
Use ! sparingly. Every use is a claim that you know something the compiler does not. If that claim is wrong, you get a NullReferenceException at runtime. A common legitimate use is when a value is assigned in a constructor or a framework callback that the compiler cannot see:
public class RequestHandler { private string _requestId; public RequestHandler() { _requestId = null!; // Assigned in Initialize() before use } public void Initialize(string requestId) { _requestId = requestId; } }
Compiler Warnings and Their Meaning
The nullable analysis produces several warning codes. The most common ones are:
| Warning | Meaning |
|---|---|
| CS8600 | Converting a null literal or possible null value to a non-nullable type |
| CS8602 | Dereferencing a possibly null reference |
| CS8604 | Passing a possibly null argument to a non-nullable parameter |
| CS8618 | Non-nullable field must contain a non-null value when exiting constructor |
CS8618 deserves attention because it fires at the end of every constructor for non-nullable fields that are not definitely assigned. This is common when a field is populated by a framework, dependency injection, or a serialization library:
public class AppSettings { public string ConnectionString { get; set; } // CS8618 }
If the framework guarantees the property will be set before use, the standard resolution is to initialize the property to string.Empty or use the null-forgiving operator. If the value genuinely may be absent, declare the property as string? and handle the null case explicitly.
Runtime Behavior: Annotations Are Not Guards
A critical point is that nullable annotations do not add runtime checks. Passing null to a method that declares a non-nullable string parameter will not throw at the call boundary. The compiler warns at the call site, but if the caller ignores the warning or comes from code where nullable is disabled, the null value flows through.
This means nullable reference types are a development-time safety feature, not a runtime validation mechanism. If you need runtime guarantees, use a guard clause such as ArgumentNullException.ThrowIfNull or a manual null check:
public void Save(string name) { ArgumentNullException.ThrowIfNull(name); // name is guaranteed non-null from here }
Combining nullable annotations with explicit runtime guards gives you both compile-time feedback and runtime safety.
Migrating an Existing Codebase
Enabling nullable on a large legacy project produces hundreds or thousands of warnings. A practical migration path is to enable the nullable context file-by-file using #nullable enable at the top of each file, starting with files that have the least external dependency. As you annotate public APIs with ? where appropriate, the compiler's analysis becomes more useful because callers see the correct contract.
During migration, you will encounter third-party libraries that do not have nullable annotations. The compiler treats their reference types as oblivious — it does not warn when you pass a possibly-null value to them, nor does it warn when they return a value that you treat as non-nullable. This is intentional: oblivious types do not participate in nullability analysis, which prevents false positives but also reduces the safety guarantees at those boundaries.
Where Nullable Analysis Falls Short
The compiler's flow analysis is conservative and does not understand every pattern. For example, checking a collection for emptiness does not convince the compiler that an element access is safe:
List<string?> items = GetItems(); if (items.Count > 0) { string first = items[0]; // CS8602: possibly null }
You must handle the null case explicitly, even when your domain logic says the element cannot be null. This is not a compiler bug; it is the analyzer erring on the side of safety. The tradeoff is that you occasionally write null checks that are logically redundant, but the alternative — the analyzer guessing — would produce false negatives that lead to runtime null dereferences.
Configuring Warnings as Errors
For teams that want strict enforcement, nullable warnings can be promoted to errors in the project file:
<PropertyGroup> <Nullable>enable</Nullable> <WarningsAsErrors>nullable</WarningsAsErrors> </PropertyGroup>
The nullable keyword in WarningsAsErrors covers all nullable-related warning codes in current .NET SDK versions. If your SDK does not recognize the keyword, use the explicit warning codes instead. This policy forces every developer on the team to resolve nullability warnings before the code compiles. It works well on new projects or after a migration is complete, but it can slow down development if the codebase still has many unresolved warnings.
The c# nullable reference type feature is most effective when the entire team treats the annotations as part of the API contract. A string? return type is a promise that callers must handle null; a string return type is a promise that they do not need to. Keeping those promises accurate is what makes the feature valuable over time.