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 required members so an object is complete when it is created.
  • Use the Try pattern for lookups. Methods such as Dictionary.TryGetValue return a bool and give the value through an out parameter. They are annotated so the compiler knows the value is not null when the method returned true: if (prices.TryGetValue("Green Fixed", out var p)). Your own methods can get the same treatment with attributes such as [NotNullWhen(true)] from System.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.