C# Nullable Reference Types
Nullable reference types help C# find possible null mistakes before your program runs. They let you mark which reference variables are expected to hold real objects and which ones are allowed to be null. This matters because many common C# crashes are NullReferenceException errors caused by using an object reference that was never assigned.
Overview: How Nullable Reference Types Work
Before C# 8, every reference type could already contain null. A string, array, class instance, delegate, or interface variable was really a reference to an object, and that reference could point to nothing. The compiler usually did not warn you when you assigned null to a string or called a method on a variable that might be null. The mistake was often discovered only at runtime.
Nullable reference types, often shortened to NRT, add a compile-time layer on top of reference types. With nullable analysis enabled, string means “this should not be null,” while string? means “this may be null.” The runtime representation is the same reference either way. Unlike int?, which is a real Nullable<int> value type, string? is not a different CLR type from string. The question mark is metadata and compiler analysis.
The compiler tracks nullability using annotations and flow analysis. An annotation is what you write in the type: Customer or Customer?. Flow analysis is the compiler’s attempt to follow your code paths. If you test if (name != null), then inside that block the compiler treats name as non-null. If a method may return null, the compiler warns when you immediately dereference the result without checking.
Nullable reference types do not remove every possible null problem. Reflection, old libraries, dynamic code, incorrectly annotated APIs, arrays, and multi-threaded mutation can still surprise you. They are best understood as a strong static warning system. They make your intent visible and catch many bugs, but the CLR still allows a reference variable to contain null.
In modern .NET projects, nullable analysis is commonly enabled in the project file with <Nullable>enable</Nullable>. In individual examples or files, you can also use #nullable enable. When enabled, the compiler reports warnings such as assigning null to a non-nullable reference or calling a member through a maybe-null reference.
Syntax
#nullable enable
string requiredName = "Ada";
string? optionalNickname = null;
if (optionalNickname is not null)
{
Console.WriteLine(optionalNickname.Length);
}
string displayName = optionalNickname ?? requiredName;
string trusted = optionalNickname!;
| Syntax | Meaning |
|---|---|
string name |
A non-nullable reference. The compiler expects it to contain a real string. |
string? name |
A nullable reference. The compiler allows null and requires checks before dereferencing. |
#nullable enable |
Turns nullable annotations and warnings on for the file. |
x is not null |
A null check that updates compiler flow analysis. |
x ?? fallback |
Uses x when it is non-null, otherwise uses the fallback expression. |
x! |
The null-forgiving operator. It tells the compiler to stop warning, but it does not check anything at runtime. |
Examples
Distinguishing Required and Optional Strings
#nullable enable
using System;
class Program
{
static void Main()
{
string title = "C# Basics";
string? subtitle = null;
Console.WriteLine(title.ToUpperInvariant());
if (subtitle is null)
{
Console.WriteLine("No subtitle");
}
else
{
Console.WriteLine(subtitle.ToUpperInvariant());
}
}
}
Output:
C# BASICS
No subtitle
The title variable is non-nullable, so the compiler lets you call ToUpperInvariant directly. The subtitle variable is nullable, so the code checks it first. In the else branch, flow analysis knows subtitle cannot be null, so calling a method on it is safe.
Returning a Maybe-Null Result
#nullable enable
using System;
class Program
{
static string? FindUserEmail(string username)
{
if (username == "maria")
{
return "maria@example.com";
}
return null;
}
static void Main()
{
string requestedUser = "lee";
string? email = FindUserEmail(requestedUser);
string message = email ?? "email not found";
Console.WriteLine($"User: {requestedUser}");
Console.WriteLine($"Email: {message}");
}
}
Output:
User: lee
Email: email not found
The method returns string? because searching may fail. This is clearer than returning an empty string and hoping every caller understands that convention. The caller uses ?? to produce safe display text without dereferencing a maybe-null value.
Designing a Class with Non-Nullable Properties
#nullable enable
using System;
class Customer
{
public Customer(string name, string? phone)
{
Name = name;
Phone = phone;
}
public string Name { get; }
public string? Phone { get; }
public string ContactLine()
{
return Phone is null ? Name : $"{Name} ({Phone})";
}
}
class Program
{
static void Main()
{
Customer first = new Customer("Amina", null);
Customer second = new Customer("Jon", "555-0134");
Console.WriteLine(first.ContactLine());
Console.WriteLine(second.ContactLine());
}
}
Output:
Amina
Jon (555-0134)
The constructor requires name, so every valid Customer has a non-null Name. Phone is explicitly optional. This is the main design benefit of nullable reference types: your type declarations explain which data is required and which data may be missing.
How It Works Step by Step
- The compiler sees nullable annotations such as
stringandstring?when nullable analysis is enabled. - It records your intent in metadata attributes so other nullable-aware C# projects can understand your public APIs.
- It analyzes assignments. Assigning
nullto a non-nullable variable creates a warning. - It analyzes dereferences. Calling
customer.Name.Lengthis accepted whencustomeris known to be non-null and warned when it may benull. - It follows control flow through checks such as
if (x is null),if (x is not null),throw,return, and pattern matching. - It does not change the CLR object model. At runtime,
stringandstring?are both object references, and either can technically benull. - If you use the null-forgiving operator
!, the compiler suppresses the warning at that expression only. No runtime validation is added.
This compile-time nature explains both the power and the limits of the feature. Nullable annotations make APIs much easier to use correctly, especially across large codebases. But they are warnings, not a new memory model. You should still validate external data, constructor arguments, deserialized values, and anything that crosses a trust boundary.
Common Mistakes
Assuming the Question Mark Creates a New Runtime Type
#nullable enable
using System;
class Program
{
static void Main()
{
string? maybeName = "Nora";
string name = "Nora";
Console.WriteLine(maybeName.GetType() == name.GetType());
Console.WriteLine(maybeName.GetType().Name);
}
}
Output:
True
String
string? and string are the same runtime type. The difference is compiler information. This is why nullable reference types help with warnings but do not prevent all nulls from existing at runtime.
Dereferencing Before Checking
#nullable enable
string? input = null;
Console.WriteLine(input.Length);
This is wrong because input may be null. With nullable analysis enabled, the compiler warns that you may be dereferencing a null reference. If executed anyway, this would throw NullReferenceException.
#nullable enable
using System;
class Program
{
static void Main()
{
string? input = null;
Console.WriteLine(input?.Length ?? 0);
}
}
Output:
0
The corrected version uses the null-conditional operator ?. and then ?? to convert a missing length into 0. A normal if (input is not null) check would also be correct.
Overusing the Null-Forgiving Operator
#nullable enable
string? value = null;
Console.WriteLine(value!.Length);
The ! operator only silences the compiler warning. It does not create an object, add a null check, or make the value safe. Use it only when you have information the compiler cannot see, such as a framework assigning a property before use.
#nullable enable
using System;
class Program
{
static void Main()
{
string? value = "ready";
if (value is not null)
{
Console.WriteLine(value.Length);
}
}
}
Output:
5
Best Practices
- Enable nullable reference types in new projects and keep the warnings visible.
- Use non-nullable reference types for required data and nullable reference types for genuinely optional data.
- Prefer constructor parameters or
requiredproperties to create valid objects with initialized non-nullable members. - Return
T?when a lookup can naturally fail, and document whatnullmeans. - Check nullable values with
is not null, pattern matching,?., or??before dereferencing. - Avoid using empty strings, fake objects, or magic values to hide missing data unless that is truly the domain rule.
- Use
!sparingly. Treat it as a local escape hatch, not a general fix for warnings. - Be careful with arrays and collections.
List<string?>means the list may contain null elements;List<string>?means the list reference itself may be null. - Validate data from JSON, databases, user input, and older libraries even when your local variables are annotated.
Practice Exercises
- Write a method named
NormalizeNamethat acceptsstring?. Return"Guest"when the argument isnullor whitespace; otherwise return the trimmed name. - Create a
Bookclass whereTitleis required andSubtitleis optional. Add a method that prints either the title alone orTitle: Subtitle. - Write a lookup method that returns
Customer?. In the caller, use anifstatement to print the customer’s name only when one was found.
Summary
- Nullable reference types are a compile-time safety feature for references such as
string, classes, arrays, delegates, and interfaces. Tmeans a reference is expected to be non-null;T?meansnullis allowed.- The CLR type does not change.
stringandstring?are both references toSystem.String. - The compiler uses annotations and flow analysis to warn about unsafe assignments and dereferences.
- Null checks,
?.,??, and pattern matching are the usual tools for safe use. - The null-forgiving operator
!suppresses warnings only; it does not make code safe at runtime. - Good nullability annotations make APIs easier to understand and reduce
NullReferenceExceptionbugs.
