Nullable Reference Types and Null Safety
Avoiding NullReferenceException in C#: nullable value types, nullable reference types and compiler warnings, the ?., ?? and ??= operators, the null-forgiving operator, and patterns for checking null.
The null problem
A variable of a reference type (a class, a string, an array) holds
either a reference to an object or null: no object at
all. The general idea of a "no value" marker, and how other
languages spell it, is on
Types and Type Systems. Null is useful (a customer may have no email address), but every
place that uses a reference must be sure it is not null. When it is,
the program stops with a
NullReferenceException: the exception .NET throws when code reads a member of, or calls a
method on, a reference that is null.
var customers = new Dictionary<string, Customer>
{
["C-1001"] = new Customer { Name = "Mei Chen", Email = "mei.chen@example.com" },
["C-1002"] = new Customer { Name = "Ravi Kumar" },
};
foreach (var (id, c) in customers)
Console.WriteLine($"{id}: {c.Email.ToLower()}");
class Customer
{
public string Name { get; set; } = "";
public string Email { get; set; }
}
C-1001: mei.chen@example.com Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
The message does not say which reference was null; the
stack trace gives only the line. Ravi's Email was never
set, so it is null, and .ToLower() on null fails. The
bug is not on the line that crashes but where the missing value came
from, which can be far away. That is why C# tries to catch these
mistakes at compile time, as the rest of this page shows. In this
example the compiler did warn, on the Email property;
see below.
Nullable value types
Value types such as int, double,
bool and DateOnly can never be null: an
int always holds a number. When "no value" is a real
possibility (a meter that has not reported a temperature yet), add a
question mark. A
nullable value type
such as int? (short for
Nullable<int>) holds either a value of the
underlying type or null.
int? temperature = null;
Console.WriteLine(temperature.HasValue);
Console.WriteLine(temperature.GetValueOrDefault());
Console.WriteLine(temperature ?? -273);
temperature = 21;
Console.WriteLine(temperature.HasValue);
Console.WriteLine(temperature.Value);
int? missing = null;
Console.WriteLine(missing + 5 == null);
Console.WriteLine(missing > 0);
Console.WriteLine(missing.Value);
False 0 -273 True 21 True False Unhandled exception. System.InvalidOperationException: Nullable object must have a value.
HasValue tells you whether there is a value, and
Value returns it, or throws when there is none.
Arithmetic with null gives null (missing + 5), and a
comparison such as missing > 0 is simply
false, which is similar to how
NULL in SQL
propagates. Nullable value types are the natural match for database
columns that allow NULL.
Nullable reference types
Reference types could always be null, which is exactly the problem.
Since C# 8, the compiler can track this for you. With
nullable reference types
enabled, a plain string means "never null" and
string? means "may be null". The compiler then follows
every variable through the code and warns when a maybe-null value is
used without a check, or when null is put somewhere it is not
allowed.
string name = "Mei Chen";
string? nickname = null;
Console.WriteLine(name.Length);
Console.WriteLine(nickname.Length);
8 Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
The program crashed, but the compiler saw it coming. Building the
same code as app.cs printed this warning (the full path
before the file name is shortened here):
app.cs(5,19): warning CS8602: Dereference of a possibly null reference.
The two question marks look the same but work differently.
int? is a separate type
(Nullable<int>) with HasValue and
Value. string? is the same
string type at run time: the ? is only an
annotation for the compiler's analysis, and nothing checks it while
the program runs.
The warnings you will meet
| Warning | Message | Typical fix |
|---|---|---|
| CS8600 | Converting null literal or possible null value to non-nullable type. |
Declare the variable as string?, or supply a
fallback with ??.
|
| CS8602 | Dereference of a possibly null reference. |
Check with is not null first, or use
?..
|
| CS8603 | Possible null reference return. | Make the return type nullable, or return a real value (such as an empty list). |
| CS8618 | Non-nullable property '…' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. |
Initialize the property, set it in the constructor, mark it
required, or make it nullable.
|
| CS8625 | Cannot convert null literal to non-nullable reference type. |
Do not pass or assign null there, or change the
type.
|
| CS8629 | Nullable value type may be null. |
Check HasValue before using
.Value.
|
The first example on this page triggered CS8618: the
Customer class declared Email as a
non-nullable string but never gave it a value. The
compiler's suggestion is the right one. Either an email is optional
(string? Email, and every use must handle null) or it
is mandatory (required string Email, see
required members).
app.cs(13,19): warning CS8618: Non-nullable property 'Email' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
string name = null;
Console.WriteLine(name is null);
Console.WriteLine(name.Length);
True Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
The nullable context
Whether the compiler performs this analysis depends on the
nullable context: the
setting that decides, for a project or a part of a file, whether
reference types are treated as nullable-aware and whether nullable
warnings are reported. New projects from dotnet new and
file-based apps enable it in the project settings:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Inside a file, #nullable disable and
#nullable enable switch the context for the lines that
follow. That is how a large older code base, written before C# 8,
can turn the feature on one file at a time. Code from before
nullable reference types has no annotations at all: there,
string means "string, possibly null", and the compiler
stays silent.
Because they are warnings, nullable problems do not stop a build,
and in a busy build log they are easy to ignore. Many teams
therefore treat warnings as errors (see
Testing and Code Quality). In a file-based app you can set that with a
#:property directive, and the same code no longer
compiles:
#:property TreatWarningsAsErrors=true
string? nickname = null;
Console.WriteLine(nickname.Length);
example.cs(3,19): error CS8602: Dereference of a possibly null reference.
Checking for null
The compiler's flow analysis understands ordinary checks. After
if (email is not null), it knows that
email is not null inside the block, and the warning
disappears. The same works with an early return.
string? email = FindEmail("C-1002");
if (email is not null)
Console.WriteLine(email.ToLower());
else
Console.WriteLine("No email on file");
Console.WriteLine(Domain(FindEmail("C-1001")));
static string? FindEmail(string customerId) =>
customerId == "C-1001" ? "mei.chen@example.com" : null;
static string Domain(string? email)
{
if (email is null)
return "(none)";
return email[(email.IndexOf('@') + 1)..];
}
No email on file example.com
Prefer is null and is not null over
== null. A class can overload == with its
own logic, whereas is null always tests the reference
itself. For strings, string.IsNullOrEmpty and
string.IsNullOrWhiteSpace combine the null check with
an emptiness check, which is usually what input validation needs.
Guard clauses
A public method that cannot work without an argument should fail
immediately, with a clear message, instead of crashing somewhere
deeper. ArgumentNullException.ThrowIfNull does that in
one line, and the compiler knows the value is not null afterward.
Print("INC-40112");
Print(null);
static void Print(string? number)
{
ArgumentNullException.ThrowIfNull(number);
Console.WriteLine(number.ToUpper());
}
INC-40112 Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'number')
Null in patterns
In a switch expression, null is a pattern of its own,
and { } (an empty property pattern) matches anything
that is not null. Type patterns such as
string s never match null, so the order of the arms
decides where null ends up (see
Control Flow and Pattern Matching).
object?[] values = ["Delft", 21.5, null, new Reading("MTR-0042", 412.5)];
foreach (var v in values)
{
string text = v switch
{
null => "nothing",
string s => $"text '{s}'",
Reading { Kwh: > 400 } r => $"high reading on {r.MeterId}",
{ } other => $"a {other.GetType().Name}",
};
Console.WriteLine(text);
}
record Reading(string MeterId, double Kwh);
text 'Delft' a Double nothing high reading on MTR-0042
The null-conditional operator
The
null-conditional operator
?. (and ?[] for indexes) reads a member
only when the value on its left is not null. Otherwise the whole
expression stops and gives null, instead of throwing. Chains such as
customer.Address?.City replace nested
if statements. JavaScript calls the same thing
optional chaining.
Customer mei = new() { Name = "Mei Chen", Address = new Address("Utrecht", "3511 AB") };
Customer ravi = new() { Name = "Ravi Kumar" };
Console.WriteLine(mei.Address?.City);
Console.WriteLine(ravi.Address?.City);
Console.WriteLine(ravi.Address?.City ?? "(no address)");
Console.WriteLine(ravi.Address?.City.Length ?? 0);
string[]? tags = null;
Console.WriteLine(tags?[0] ?? "(no tags)");
class Customer
{
public required string Name { get; init; }
public Address? Address { get; set; }
}
record Address(string City, string PostalCode);
Utrecht (no address) 0 (no tags)
The second line prints an empty line: null converts to empty text in
Console.WriteLine. Note the result type: when
City.Length is an int,
Address?.City.Length is an int?, because
it may be null. Combining ?. with ??
turns it back into a plain value. Calling an event or callback that
may be null is a classic use:
Changed?.Invoke(this, EventArgs.Empty) (see
Delegates, Lambdas and Events).
Null-conditional assignment
Since C# 14, ?. also works on the left side of an
assignment: customer?.Notes = "…" assigns only when
customer is not null, and does nothing otherwise.
Customer? mei = FindCustomer("C-1001");
Customer? nobody = FindCustomer("C-9999");
mei?.Notes = "Prefers email";
nobody?.Notes = "never stored";
Console.WriteLine(mei?.Notes);
Console.WriteLine(nobody?.Notes ?? "(no customer)");
static Customer? FindCustomer(string id) =>
id == "C-1001" ? new Customer { Name = "Mei Chen" } : null;
class Customer
{
public required string Name { get; init; }
public string? Notes { get; set; }
}
Prefers email (no customer)
The null-coalescing operators
The
null-coalescing operator
?? returns its left operand when that is not null, and
otherwise its right operand: a fallback value in one expression. The
assignment form ??= assigns the right side only when
the variable is currently null, which is handy for creating
something on first use. JavaScript has the same operators as
nullish coalescing.
string? preferred = null;
string display = preferred ?? "Customer";
Console.WriteLine(display);
decimal? discount = null;
decimal price = 0.31m - (discount ?? 0m);
Console.WriteLine(price);
List<string>? notes = null;
notes ??= [];
notes.Add("Called customer");
notes ??= ["never used"];
Console.WriteLine(string.Join(", ", notes));
Customer 0.31 Called customer
Unlike Python's or and JavaScript's ||,
?? only reacts to null: an empty string or a zero is a
real value and is kept.
Throwing as the fallback
The right side of ?? may be a
throw expression. That gives a compact way to reject
null when storing a constructor argument:
var ticket = new Ticket("INC-40112", "Amara Okafor");
Console.WriteLine(ticket.AssignedTo);
var broken = new Ticket("INC-40113", null);
class Ticket(string number, string? assignedTo)
{
public string Number { get; } = number;
public string AssignedTo { get; } =
assignedTo ?? throw new ArgumentNullException(nameof(assignedTo));
}
Amara Okafor Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'assignedTo')
The null-forgiving operator
Sometimes you know more than the compiler. The
null-forgiving operator, a ! after an expression (value!), tells
the compiler "trust me, this is not null" and removes the warning.
It does nothing at run time: if you are wrong, the program fails
exactly as it would without the annotation.
var lookup = new Dictionary<string, string> { ["C-1001"] = "mei.chen@example.com" };
string known = Find("C-1001")!;
Console.WriteLine(known.Length);
string unknown = Find("C-9999")!;
Console.WriteLine(unknown.Length);
string? Find(string id) => lookup.GetValueOrDefault(id);
20 Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
Use ! rarely and deliberately, for example in a unit
test, or right after a check the compiler cannot follow. Every
! is a place where the null analysis has been switched
off; a code reviewer should be able to see why it is safe. In most
cases a real check (is not null, ??, or a
guard clause) is the better fix.
Designing code that avoids null
The best null check is the one you never need. A few habits keep nulls rare and visible:
-
Annotate honestly. Mark a property, parameter or
return value with
?only when null is a meaningful value. Everything else is non-nullable, and the compiler holds callers to it. - Return empty collections, not null. A method that finds no outage tickets should return an empty list; callers can then loop over the result without a check.
-
Make mandatory data impossible to forget. Use
constructors and
requiredmembers so an object is complete when it is created. -
Use the Try pattern for lookups. Methods such as
Dictionary.TryGetValuereturn abooland give the value through anoutparameter. They are annotated so the compiler knows the value is not null when the method returnedtrue:if (prices.TryGetValue("Green Fixed", out var p)). Your own methods can get the same treatment with attributes such as[NotNullWhen(true)]fromSystem.Diagnostics.CodeAnalysis. - Check at the edges. Data from users, files, JSON, databases and older libraries can contain null whatever your types say. Validate it once where it enters, and let the inside of the program rely on the annotations.