Nullable Reference Types
By the Syncfusion DevTutorials team · Last updated
Make null visible to the compiler: nullable context, string vs string?, warnings, null checks, !, ??, ??=, nullable attributes, legacy code and avoiding NullReferenceException.
Introduction to Nullable Reference Types
Think about a form with some boxes marked required and some marked optional.
Whoever reads the form knows straight away: the "Name" box always has a value, but the "Middle name" box might be empty. Nobody is surprised by an empty optional box.
Old C# code had no such marks. Any string, any Customer, any object reference could secretly be null. If you forgot to check, your program crashed with the most common .NET error of all:
System.NullReferenceException: Object reference not set to an instance of an object.
Nullable reference types add labels to mark which types can be null and which cannot:
stringmeans "never null".string?means "might be null".
The compiler then checks your code and warns you whenever you might use a null by mistake, before the program runs.
In simple words: ? says "this can be empty", and the compiler makes sure you check before you use it.
Easy Way to Remember: no ? = always has a value. ? = check first.
What You Will Learn
In this lesson, you will learn:
- How to turn on the nullable context.
- The difference between
stringandstring?. - What the nullability warnings mean.
- Null checks the compiler understands.
- The null-forgiving operator
!. - The null-coalescing operators
??and??=. - Nullable attributes such as
[NotNullWhen]. - How to add nullability to older code.
- Habits that prevent
NullReferenceException.
All examples run in your browser. Nullable warnings are switched off by default in this editor, so each example starts with #nullable enable. Warnings show as yellow squiggles: hover over one to read it. A warning does not stop the program running.
Start With a Working Example
Press Run, then hover over the squiggly line in the editor.
The compiler warns before the crash
#nullable enable
using System;
string name = "Asha";
string? nickname = FindNickname("Asha"); // might be null
Console.WriteLine($"Name length: {name.Length}");
// Warning CS8602: Dereference of a possibly null reference.
// The compiler knows nickname might be null here.
try
{
Console.WriteLine($"Nickname length: {nickname.Length}");
}
catch (NullReferenceException)
{
Console.WriteLine("Crash! The warning was right.");
}
// The fix: check first.
if (nickname is not null)
Console.WriteLine($"Nickname length: {nickname.Length}");
else
Console.WriteLine("No nickname");
static string? FindNickname(string name) => null;
The compiler marked nickname.Length with a warning before you ran anything. Running it proved the warning right. The checked version below it has no warning.
Nullable Context
The nullable context is the switch that turns this feature on. When it is on, the compiler treats string as non-null and string? as nullable, and it gives warnings.
There are three ways to turn it on:
1. For the whole project, in the .csproj file. New projects created with dotnet new already have this:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
2. For one file or part of a file, with a directive:
#nullable enable // on from here
#nullable disable // off from here
#nullable restore // back to the project setting
3. Warnings only, or annotations only, for gradual migration:
#nullable enable warnings // warn, but treat old code as "unknown"
#nullable enable annotations // allow ? marks, but no warnings yet
Turn nullable on for every new project. It costs nothing at runtime. It only changes what the compiler checks, and it catches a whole class of bugs for free.
Nullable References
With the context on, every reference type has two forms:
| Write | Meaning | Can assign null? | Can use .Member straight away? |
|---|---|---|---|
string | Never null | No (warning) | Yes |
string? | Maybe null | Yes | No; check first (warning) |
Customer | Never null | No (warning) | Yes |
Customer? | Maybe null | Yes | No; check first (warning) |
This is the same ? you already know from nullable value types such as int?. There is one big difference:
int?is a different runtime type:Nullable<int>, with.HasValueand.Value.string?is the same runtime type asstring. The?is only a note for the compiler.
Nullable value types versus nullable reference types
#nullable enable
using System;
int? age = null; // Nullable<int>: a real wrapper type
Console.WriteLine($"age.HasValue: {age.HasValue}");
Console.WriteLine($"int? runtime type: {typeof(int?).Name}");
string? city = null; // still just a string at runtime
Console.WriteLine($"city is null: {city is null}");
Console.WriteLine($"string? runtime type: {typeof(string).Name}");
city = "Pune";
Console.WriteLine($"Now city has {city.Length} letters"); // no warning: the compiler saw the assignment
The last line has no warning. The compiler tracks the value: after city = "Pune", it knows city is not null. This tracking is called flow analysis.
Nullability Warnings
Warnings about nullability all have numbers starting with CS86. These are the main ones you will see:
| Warning | Means | Typical fix |
|---|---|---|
| CS8600 | Converting a possible null to a non-nullable type | Make the variable ?, or give a default with ?? |
| CS8602 | Dereference of a possibly null reference | Check for null first |
| CS8603 | Possible null reference return | Return a real value, or make the return type ? |
| CS8604 | Possible null argument for a non-nullable parameter | Check first, or make the parameter ? |
| CS8618 | Non-nullable property is not set by the constructor | Initialise it, add required, or make it ? |
| CS8625 | Cannot convert null literal to non-nullable type | Do not assign null, or make it ? |
Common warnings in one place
#nullable enable
using System;
var customer = new Customer("Ravi");
Console.WriteLine(customer.Describe());
string? input = Console.ReadLine(); // null when there is no input
string city = input ?? "Unknown"; // fixed with ?? (no CS8600)
Console.WriteLine($"City: {city}");
class Customer
{
public string Name { get; }
public string? Email { get; set; } // optional: fine to leave null
public string Phone { get; set; } = "";// required-ish: given a default (no CS8618)
public Customer(string name) => Name = name;
public string Describe()
{
// Email.Length here would be CS8602, so we check:
string email = Email ?? "(no email)";
return $"{Name}, {email}, phone '{Phone}'";
}
}
Nullable warnings are only warnings. The program still compiles, and a null can still sneak in at runtime from old code, JSON or reflection. Many teams turn the warnings into errors with <WarningsAsErrors>nullable</WarningsAsErrors>, so they cannot be ignored.
Null Checks
The compiler's flow analysis understands normal null checks. After a check, it knows the variable is safe, and the warning goes away.
The compiler understands these null checks:
if (x is not null) { ... }if (x != null) { ... }if (x is null) return;(an early exit)if (x is { } value) { ... }(a property pattern)if (string.IsNullOrEmpty(x)) return;ArgumentNullException.ThrowIfNull(x);
Different null checks
#nullable enable
using System;
PrintLength("Chennai");
PrintLength(null);
Greet(new User("Meera"));
Greet(null);
static void PrintLength(string? text)
{
if (text is null)
{
Console.WriteLine("No text");
return; // early exit
}
Console.WriteLine($"{text} has {text.Length} letters"); // safe here
}
static void Greet(User? user)
{
if (user is { Name: var name }) // pattern: not null, and grab Name
Console.WriteLine($"Hello, {name}");
else
Console.WriteLine("Hello, guest");
}
record User(string Name);
The Null-Conditional Operator ?.
?. safely reads a member only if the object is not null. If the object is null, the entire expression becomes null instead of crashing.
Safe member access with ?.
#nullable enable
using System;
Order? found = new Order(new Address("Kochi"));
Order? missing = null;
Console.WriteLine(found?.ShipTo?.City ?? "no city");
Console.WriteLine(missing?.ShipTo?.City ?? "no city");
int? length = missing?.ShipTo?.City.Length; // int? because the chain might stop
Console.WriteLine($"Length: {length?.ToString() ?? "null"}");
record Address(string City);
record Order(Address? ShipTo);
Checking Arguments
For public methods, reject null arguments at the start. Then the rest of the method knows these arguments are safe to use.
public void Ship(Order order, string address)
{
ArgumentNullException.ThrowIfNull(order);
ArgumentException.ThrowIfNullOrWhiteSpace(address);
...
}
Null-Forgiving Operator (!)
Sometimes you know a value is not null, but the compiler cannot prove it. The null-forgiving operator ! tells the compiler: "trust me, this is not null." It removes the warning.
Telling the compiler you know better
#nullable enable
using System;
using System.Collections.Generic;
var prices = new Dictionary<string, decimal> { ["Pen"] = 10m, ["Notebook"] = 60m };
// Find returns string? because it might find nothing...
string? first = new List<string> { "Pen", "Notebook" }.Find(x => x.StartsWith("P"));
// ...but we just put "Pen" in the list, so we know it is found.
Console.WriteLine($"Found: {first!.ToUpper()}");
// A bad use of ! : this really can be null.
string? missing = new List<string> { "Pen" }.Find(x => x.StartsWith("Z"));
try
{
Console.WriteLine(missing!.ToUpper()); // no warning, but still a crash
}
catch (NullReferenceException)
{
Console.WriteLine("! only hides the warning. It does not make the value safe.");
}
! changes nothing at runtime. It only silences the compiler. If you are wrong, you still get a NullReferenceException. Every ! in your code is a promise you must be sure of. Prefer a real check whenever you can.
null! is a common pattern for a field that a framework fills in later, such as an Entity Framework navigation property:
public Customer Customer { get; set; } = null!; // EF sets this when loading
Null-Coalescing Operator (??)
?? provides a fallback value when the left side is null.
result = value ?? fallback;
- If
valueis not null,resultisvalue. - If
valueis null,resultisfallback.
The result of x ?? "default" is always non-nullable. Use ?? to convert a string? into a string.
Fallbacks with ??
#nullable enable
using System;
using System.Collections.Generic;
var settings = new Dictionary<string, string?>
{
["Theme"] = "Dark",
["Language"] = null,
};
string theme = settings.GetValueOrDefault("Theme") ?? "Light";
string language = settings.GetValueOrDefault("Language") ?? "en-IN";
string font = settings.GetValueOrDefault("Font") ?? "Segoe UI";
Console.WriteLine($"{theme}, {language}, {font}");
// ?? can be chained: the first non-null wins
string? fromUser = null, fromCompany = null, fromSystem = "UTC+05:30";
Console.WriteLine($"Time zone: {fromUser ?? fromCompany ?? fromSystem ?? "UTC"}");
// ?? can throw when a value is required
try
{
string apiKey = settings.GetValueOrDefault("ApiKey") ?? throw new InvalidOperationException("ApiKey is missing");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Null-Coalescing Assignment (??=)
??= assigns a value only if the variable is currently null.
x ??= value; // same as: if (x is null) x = value;
It is perfect for lazy initialisation: creating something the first time it is needed.
Create on first use with ??=
#nullable enable
using System;
using System.Collections.Generic;
var cache = new PriceCache();
Console.WriteLine(cache.Get("Pen"));
Console.WriteLine(cache.Get("Notebook"));
Console.WriteLine(cache.Get("Pen"));
class PriceCache
{
private Dictionary<string, decimal>? _prices; // not loaded yet
public decimal Get(string item)
{
_prices ??= Load(); // loads only the first time
return _prices[item];
}
private static Dictionary<string, decimal> Load()
{
Console.WriteLine("(loading prices)");
return new() { ["Pen"] = 10m, ["Notebook"] = 60m };
}
}
| Operator | Name | Does |
|---|---|---|
x?.Y | Null-conditional | Reads Y only if x is not null; otherwise gives null |
x ?? y | Null-coalescing | Gives x, or y if x is null |
x ??= y | Null-coalescing assignment | Sets x = y only if x is null |
x! | Null-forgiving | Tells the compiler x is not null; no runtime effect |
Nullable Annotations
Sometimes ? is not enough to describe how a method behaves. Think about int.TryParse-style methods: the output is null when the method returns false, and not null when it returns true.
Nullable attributes in System.Diagnostics.CodeAnalysis describe these rules, so the compiler understands your methods:
| Attribute | Put it on | Means |
|---|---|---|
[NotNullWhen(true)] | An out or nullable parameter | Not null when the method returns true |
[MaybeNullWhen(false)] | An out parameter | May be null when the method returns false |
[NotNull] | A parameter | Will not be null after the method returns (it throws otherwise) |
[MemberNotNull(nameof(X))] | A method | Field or property X is not null after this method |
[NotNullIfNotNull("input")] | A return value | Not null if input was not null |
[DoesNotReturn] | A method | Never returns normally (always throws) |
Teach the compiler about your TryFind method
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
var phoneBook = new PhoneBook();
if (phoneBook.TryFind("Asha", out string? number))
Console.WriteLine($"Asha: {number.Length} digits, {number}"); // no warning: number is not null here
else
Console.WriteLine("Asha not found");
if (!phoneBook.TryFind("Zoya", out string? other))
Console.WriteLine($"Zoya not found (value is {other ?? "null"})");
class PhoneBook
{
private readonly Dictionary<string, string> _numbers = new() { ["Asha"] = "9840012345" };
public bool TryFind(string name, [NotNullWhen(true)] out string? number)
=> _numbers.TryGetValue(name, out number);
}
Without [NotNullWhen(true)], number.Length inside the if would get a CS8602 warning, because number is declared string?. The attribute tells the compiler how the method behaves.
Required Members
For properties that must be set when an object is created, the cleanest fix for warning CS8618 is the required modifier:
required fixes CS8618 without fake defaults
#nullable enable
using System;
var product = new Product { Name = "Desk Lamp", Sku = "DL-204" };
Console.WriteLine($"{product.Name} ({product.Sku}), note: {product.Note ?? "none"}");
// var bad = new Product { Name = "Lamp" }; // error CS9035: required member 'Sku' must be set
class Product
{
public required string Name { get; init; }
public required string Sku { get; init; }
public string? Note { get; init; }
}
Working with Legacy Code
Most real projects have old code written before nullable reference types. Turning the feature on for a big project at once can produce thousands of warnings. Migrate step by step instead.
A Safe Migration Plan
- Turn on warnings for new files only. Add
#nullable enableat the top of each new file, or each file you touch. - Start at the bottom. Annotate small, shared classes first (models, helpers). Code that uses them gets better warnings for free.
- Annotate public APIs. Decide for every parameter and return value: can this be null? Add
?where yes. - Fix warnings, do not hide them. Use checks and
??before reaching for!. - Flip the project switch to
<Nullable>enable</Nullable>when most files are done. - Make nullable warnings errors to keep it clean.
Oblivious Code
Code compiled with nullable off is called oblivious. The compiler does not know whether its types can be null or not, so you get no warnings when you use them. Treat values from oblivious code as possibly null and check them.
Mixing old and new code in one file
#nullable disable
static class LegacyLookup
{
// Written before nullable reference types: may return null with no warning.
public static string FindManager(string employee) => employee == "Asha" ? "Ravi" : null;
}
#nullable enable
static class Program
{
static void Main()
{
foreach (string employee in new[] { "Asha", "Kiran" })
{
string? manager = LegacyLookup.FindManager(employee); // treat old results as maybe-null
Console.WriteLine($"{employee} reports to {manager ?? "nobody yet"}");
}
}
}
The FindManager method can return null but says string. The compiler cannot help you there, so the new code stores the result in a string? and checks it.
Preventing Null Reference Exceptions
Nullable reference types catch most problems at compile time. A few habits close the rest of the gaps:
- Turn nullable on in every project, and treat its warnings as errors.
- Prefer non-null designs. Return an empty list, not
null. Use a "null object" (like aNullLogger) instead ofnull. - Validate at the edges. Check arguments in public methods with
ArgumentNullException.ThrowIfNull. Check data from JSON, databases and users as it enters your program. - Use
requiredand constructors so objects are complete when created. - Use
?.,??and??=for optional values instead of longifchains. - Use
!rarely, and only with a comment that explains why it is safe. - Use
TryXxxpatterns with[NotNullWhen]for lookups that might fail.
Return empty, not null
#nullable enable
using System;
using System.Collections.Generic;
foreach (string customer in new[] { "Asha", "New Customer" })
{
IReadOnlyList<string> orders = GetOrders(customer);
Console.WriteLine($"{customer}: {orders.Count} order(s)"); // no null check needed
}
static IReadOnlyList<string> GetOrders(string customer) =>
customer == "Asha"
? new[] { "ORD-1001", "ORD-1007" }
: Array.Empty<string>(); // empty, never null
Callers never have to ask "is it null?" because the answer is always a list, maybe an empty one.
Where Nullable Reference Types Are Used
- All new .NET projects, which enable
<Nullable>by default. - The .NET libraries themselves, which are fully annotated.
- ASP.NET Core model binding and validation, where non-nullable properties are treated as required.
- Entity Framework Core, which makes non-nullable string properties
NOT NULLcolumns. - Library authors, who publish annotations so their users get warnings too.
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Using a string? without checking | Warning CS8602, then NullReferenceException | Check with is not null, or use ?. and ?? |
! to hide a warning on a value that can be null | Crash with no warning | Replace ! with a real check |
| Non-nullable property not initialised | Warning CS8618 | required, a constructor, or a default value |
Returning null from a method typed string | Warning CS8603 | Return string?, or a real value |
Expecting string? to have .HasValue | Compile error | That is only for int? and other value types |
| Turning nullable on for a huge project at once | Thousands of warnings | Migrate file by file with #nullable enable |
Trusting a legacy library's string return | Unexpected null at runtime | Treat oblivious results as string? |
Returning null for an empty list | Callers forget to check and crash | Return an empty collection |
What to Remember
- Nullable reference types make "might be null" visible:
stringnever null,string?maybe null. - Turn them on with
<Nullable>enable</Nullable>or#nullable enable. - The compiler tracks values and warns with CS86xx codes before the program runs.
- Check with
is null,is not null, patterns,?.or early returns. !only silences the compiler; it does not make a value safe.??supplies a fallback;??=assigns only when the value is null.- Attributes such as
[NotNullWhen(true)]describe tricky methods to the compiler. - Migrate old code file by file, and treat oblivious values as possibly null.
- Prefer designs where null cannot happen:
required, empty collections and null objects.
Next: Logging