Nullable Reference Types
Vaibhav • September 11, 2025
In earlier chapters, we explored value types like int
, bool
, and DateTime
, and how they can be made
nullable using int?
or bool?
. But reference
types - like string
, object
, or custom classes
- have always been nullable by default. That flexibility comes with risk: null reference exceptions are one of
the most common runtime errors in C#.
Starting with C# 8.0, the language introduced nullable reference types as a way to make nullability explicit and safer. This article walks through what they are, how to enable them, how they change your code, and how to use them effectively to avoid bugs and clarify intent.
What are Nullable Reference Types?
Nullable reference types are not a new kind of type - they’re a language feature that changes how the compiler interprets your code. When enabled, the compiler distinguishes between:
string
- a non-nullable reference type (should never be null)string?
- a nullable reference type (may be null)
This distinction allows the compiler to warn you when you might be dereferencing a null value, or assigning null to a non-nullable variable.
Nullable reference types are a compile-time feature. They don’t change runtime behavior - they help you catch potential null issues before your code runs.
Enabling Nullable Reference Types
To use nullable reference types, you need to enable them in your project. You can do this in your .csproj
file:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Or you can enable it per file using a directive:
#nullable enable
Once enabled, the compiler starts analyzing your code for null safety based on your annotations.
Declaring Nullable and Non-Nullable References
With nullable reference types enabled, you must be explicit about whether a reference can be null:
string name = "Vaibhav"; // Non-nullable
string? nickname = null; // Nullable
If you try to assign null
to a non-nullable reference, the compiler will warn
you:
string name = null; // ⚠️ Warning: possible null assignment
Nullability Warnings and Flow Analysis
The compiler uses flow analysis to track whether a variable might be null at a given point. Consider this example:
string? message = GetMessage();
Console.WriteLine(message.Length); // ⚠️ Warning: possible null dereference
Since message
is nullable, accessing Length
directly is risky. You can fix this by checking for null:
if (message != null)
{
Console.WriteLine(message.Length); // ✅ Safe
}
Note: The compiler tracks null checks and adjusts its warnings accordingly. This helps you write safer code without needing runtime checks everywhere.
Using the Null-Forgiving Operator (!)
Sometimes you know a value isn’t null, even if the compiler isn’t sure. You can use the null-forgiving operator
!
to suppress the warning:
string? title = GetTitle();
Console.WriteLine(title!.Length); // Tells compiler: "trust me, it's not null"
Use this sparingly - it’s a way to silence the compiler, but if you’re wrong, you’ll still get a runtime exception.
Prefer null checks over using !
. Only use the null-forgiving
operator when you’re absolutely sure the value is not null.
Nullable Parameters and Return Types
You can annotate method parameters and return types to indicate nullability:
string? FindUser(string? username)
{
if (username == null) return null;
return "User: " + username;
}
This makes your method’s contract clear: it accepts a nullable input and may return a nullable result.
Nullability in Fields and Properties
Class fields and properties also benefit from nullability annotations. Consider this class:
class Person
{
public string Name { get; set; } // Must be non-null
public string? Nickname { get; set; } // May be null
}
If you forget to initialize Name
, the compiler will warn you. You can fix it
with a constructor or default value.
Suppressing Warnings with #nullable directives
You can temporarily disable nullable warnings in a file or region using directives:
#nullable disable
string risky = null; // No warning
#nullable restore
string safe = null; // ⚠️ Warning again
This is useful when migrating legacy code or integrating third-party libraries.
Interoperability with Legacy Code
Nullable reference types are opt-in. If you’re working with older code that doesn’t use them, the compiler
assumes all reference types are nullable. You can annotate external APIs using #nullable
directives or attributes like [MaybeNull]
and [NotNull]
.
Nullable reference types don’t change the runtime behavior of your program. They only affect compiler warnings and static analysis.
Summary
Nullable reference types help you write safer, clearer code by making nullability explicit. With string
vs string?
, the compiler can warn you
about risky assignments and dereferences. You can enable the feature project-wide or per file, annotate
variables, parameters, and return types, and use flow analysis to guide safe usage. The null-forgiving operator
!
lets you override warnings when needed, but should be used cautiously.
By adopting nullable reference types, you reduce the risk of null reference exceptions and make your code’s intent more obvious to both the compiler and other developers. In the next article, we’ll explore Pattern Matching - a powerful feature that lets you write cleaner, more expressive conditionals using modern C# syntax.