Nullable Types

null means "no value". It's useful — a customer may have no middle name, a search may find nothing — but reading a member of null crashes with a NullReferenceException. C# tackles this from two sides: nullable value types let numbers and other structs be null when you need it, and nullable reference types make the compiler warn you wherever a null could slip through.

Nullable value types

Value types like int and DateTime can't normally be null. Add ? to allow it — int? is short for Nullable<int>:

C#
int? rating = null;
Console.WriteLine(rating.HasValue);
Console.WriteLine(rating ?? 0);
Console.WriteLine(rating.GetValueOrDefault(-1));

rating = 4;
Console.WriteLine(rating.HasValue);
Console.WriteLine(rating.Value);
Console.WriteLine(rating + 1);

int? missing = null;
Console.WriteLine(missing + 1 is null);
Console.WriteLine(missing > 3);
Output
False
0
-1
True
4
5
True
False

Arithmetic with a null operand gives null, and comparisons with null are false. Reading .Value when there's no value throws an InvalidOperationException, so test first or use ??.

Nullable value types are ideal for optional data, such as a database column that may be empty:

C#
var p1 = new Patient("Ana", 1990);
var p2 = new Patient("Ben", null);

foreach (var p in new[] { p1, p2 })
{
    string age = p.BirthYear is int year ? $"{2026 - year}" : "unknown";
    Console.WriteLine($"{p.Name}: age {age}");
}

record Patient(string Name, int? BirthYear);
Output
Ana: age 36
Ben: age unknown

Nullable reference types

Reference types such as string and classes could always hold null. With nullable reference types — on by default in every new project and single-file app — the compiler tracks which variables might be null:

  • string means "should never be null".
  • string? means "might be null".

The compiler then warns you about code that could dereference null or store null where it doesn't belong. These are warnings, not errors, so the program still builds — but each one is a potential crash:

C#
string? middleName = GetMiddleName();
string copy = middleName;
Console.WriteLine(middleName.Length);

static string? GetMiddleName() => null;
Output
Program.cs(2,15): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(3,19): warning CS8602: Dereference of a possibly null reference.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.

Flow analysis: checking removes the warning

The compiler follows your code. After a null check, it knows the variable is safe:

C#
PrintLength("hello");
PrintLength(null);

static void PrintLength(string? text)
{
    if (text is null)
    {
        Console.WriteLine("(no text)");
        return;
    }
    Console.WriteLine(text.Length);   // no warning: text can't be null here
}
Output
5
(no text)

Prefer is null and is not null to == null: they can't be affected by an overloaded == operator.

The null operators

OperatorMeaning
a?.BAccess B only if a isn't null; otherwise the result is null
a?[i]Index only if a isn't null
a ?? ba if it isn't null, otherwise b
a ??= bAssign b to a only if a is null
a?.B = cAssign only if a isn't null (C# 14)
a!"Trust me, this isn't null" — silences the warning
C#
Customer? customer = FindCustomer("c-42");
Customer? nobody = FindCustomer("c-99");

Console.WriteLine(customer?.Address?.City ?? "no city");
Console.WriteLine(nobody?.Address?.City ?? "no city");
Console.WriteLine(nobody?.Orders?[0] ?? "no orders");

customer?.Address?.City = "Porto";   // C# 14: null-conditional assignment
nobody?.Address?.City = "Porto";     // does nothing, no crash
Console.WriteLine(customer?.Address?.City);

List<string>? tags = null;
tags ??= [];
tags.Add("vip");
Console.WriteLine(tags.Count);

static Customer? FindCustomer(string id) =>
    id == "c-42" ? new Customer { Address = new Address { City = "Lisbon" }, Orders = ["A1"] } : null;

class Customer
{
    public Address? Address { get; set; }
    public List<string>? Orders { get; set; }
}

class Address
{
    public string City { get; set; } = "";
}
Output
Lisbon
no city
no orders
Porto
1

Chained ?. stops at the first null, so customer?.Address?.City never crashes, whatever is missing.

Non-nullable properties must be initialised

If a class has a non-nullable reference property, the compiler wants proof it gets a value — otherwise it would start as null:

C#
var u = new User();
Console.WriteLine(u.Email.Length);

class User
{
    public string Email { get; set; }
}
Output
Program.cs(6,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.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.

Fix it in whichever way matches the meaning:

  • give it a default: public string Email { get; set; } = "";
  • set it in a constructor;
  • mark it required so callers must supply it;
  • or declare it string? if "no email" is a valid state.

The null-forgiving operator

Sometimes you know better than the compiler — for instance, a value was validated earlier in a way it can't see. A postfix ! suppresses the warning:

C#
var settings = new Dictionary<string, string?> { ["theme"] = "dark" };
string theme = settings["theme"]!;     // we just put it there
Console.WriteLine(theme.ToUpper());
Output
DARK

Use ! rarely. It doesn't check anything at run time; if you're wrong, you still get a NullReferenceException — you've just removed the warning that would have told you.

Guarding public methods

Annotations only produce warnings, and callers in other projects may ignore them. Public methods that require a non-null argument should still check, and ArgumentNullException.ThrowIfNull makes it a one-liner:

C#
Console.WriteLine(Initials("Grace Hopper"));
Console.WriteLine(Initials(null!));

static string Initials(string fullName)
{
    ArgumentNullException.ThrowIfNull(fullName);
    return string.Concat(fullName.Split(' ').Select(part => part[0]));
}
Output
GH
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'fullName')

Similar helpers exist for other checks: ArgumentException.ThrowIfNullOrEmpty, ThrowIfNullOrWhiteSpace, and ArgumentOutOfRangeException.ThrowIfNegative.

Try it yourself

Write FindUser, which returns null when no user matches, and handle both cases without any warnings.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Ben Okafor (Lagos)
Ana Lima (city not given)
zoe: not found