How it works
The explanation, with examples you can run.
What the compiler checks
string? FindName(int id) => id == 1 ? "Ada" : null;string? name = FindName(2);Console.WriteLine(name.Length);Building this with .NET 10 printed warning CS8602: Dereference of a possibly null reference, and running it threw NullReferenceException on line 3. The fix is to handle the null case where it can occur:
if (name is null) { Console.WriteLine("unknown"); return; }Console.WriteLine(name.Length); // no warning: the compiler knows name is not null hereConsole.WriteLine(name?.Length ?? 0); // or: null-conditional plus a defaultThe compiler tracks a null state ("not null" or "maybe null") for every variable through branches, pattern matches and assignments. Common warnings are CS8600 (converting a possible null to a non-nullable type), CS8602 (dereferencing a maybe-null value), CS8618 (a non-nullable property not set by the constructor) and CS8625 (passing null where it isn't allowed).
Telling the compiler what you know
Some APIs have contracts flow analysis can't see. Attributes in System.Diagnostics.CodeAnalysis describe them:
bool TryGet(string key, [NotNullWhen(true)] out Customer? customer);[return: NotNullIfNotNull(nameof(input))] string? Trim(string? input);[MemberNotNull(nameof(_conn))] void EnsureOpen();After if (TryGet(key, out var c)), c is treated as not null inside the if. The required modifier (C# 11) is the clean answer to CS8618 for properties that callers must set in an object initializer.
The null-forgiving operator ! (as in name!.Length) suppresses a warning and generates nothing. Use it only when you know more than the compiler, and leave a comment saying why.
Where nulls still get in
Annotations are a promise, not a guarantee. Values can still be null at run time when they come from:
- deserialisation:
System.Text.Jsoncan leave a non-nullable property null if the JSON omits it, unless the property isrequiredor you validate; - libraries compiled without annotations, whose types are "oblivious" and produce no warnings either way;
defaultof a struct containing reference fields, and uninitialised array elements.
So check arguments at public boundaries (ArgumentNullException.ThrowIfNull(arg)), and let the annotations protect everything inside.
Turning it on
New projects contain <Nullable>enable</Nullable>. For an existing codebase, enable it file by file with #nullable enable, annotate from the leaves (models, utilities) upwards, and then add <WarningsAsErrors>nullable</WarningsAsErrors> so new code can't regress.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.