Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 9 – Null safety

Nullable reference types and flow analysis

Turn on C# nullable reference types, read the warnings CS8600, CS8602, CS8618 and CS8625, and let flow analysis prove a value is not null before it is used.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: string? says a reference may be null, and the compiler checks the rest · Flow analysis proves a value is not null before it is read. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Switch on nullable reference types and explain what CS8600, CS8602, CS8618 and CS8625 report
  • Write null checks that flow analysis follows, so values are proven not null before they are read
  • Use the null-forgiving operator only where you can show the value is not null
  • Choose T?, class, struct or notnull so that generic code says exactly what may be null
  • Configure nullable contexts and nullable warnings as errors for a project and for single files

Before you start

What you’ll learn · Before you start

What you will learn

  • Switch on nullable reference types and explain what CS8600, CS8602, CS8618 and CS8625 report
  • Write null checks that flow analysis follows, so values are proven not null before they are read
  • Use the null-forgiving operator only where you can show the value is not null
  • Choose T?, class, struct or notnull so that generic code says exactly what may be null
  • Configure nullable contexts and nullable warnings as errors for a project and for single files

Before you start

On this page

An export of 1,200 customers stopped at row 412 with NullReferenceException: one customer had no email address, and the code called customer.Email.Trim(). The type string had always allowed null, and nothing at compile time said which strings might be missing.

Nullable reference types put that information into the types, and the compiler checks it. You will turn them on, read the warnings they raise, and write null checks that the compiler can follow, so that a build without warnings means every read was checked.

string? says a reference may be null, and the compiler checks the rest

With nullable reference types on, string means a string that is never null, and string? one that may be. New projects have had them on since the .NET 6 templates, through <Nullable>enable</Nullable> in the project file, and so do file-based apps: the project that dotnet project convert writes for one sets it too. warnings/Program.cs breaks the rules in four ways, and the session builds and runs it:

warnings/Program.cs C# · warnings/Program.cs
// Four kinds of nullable warning. The build still succeeds, and the program runs.
string? found = Lookup("C-101");
string missing = Lookup("C-102");
Console.WriteLine($"found has {found.Length} characters");
Send(null);
Console.WriteLine($"City is null: {new Customer().City is null}");
Console.WriteLine($"missing has {missing.Length} characters");

static string? Lookup(string id) => id == "C-101" ? "asha@example.com" : null;

static void Send(string address) => Console.WriteLine($"sending to {address ?? "nobody"}");

class Customer
{
    public string City { get; set; }
}
Building and running warnings Python · warnings_session.py
from session import run

# warnings builds with four kinds of nullable warning, and then runs until it reads the null it was warned about.
run("""
cd warnings
dotnet build -v q
!dotnet bin/Debug/net10.0/App.dll
""")

Output

$ cd warnings
$ dotnet build -v q
warnings/Program.cs(15,19): warning CS8618: Non-nullable property 'City' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [warnings/App.csproj]
warnings/Program.cs(3,18): warning CS8600: Converting null literal or possible null value to non-nullable type. [warnings/App.csproj]
warnings/Program.cs(4,32): warning CS8602: Dereference of a possibly null reference. [warnings/App.csproj]
warnings/Program.cs(5,6): warning CS8625: Cannot convert null literal to non-nullable reference type. [warnings/App.csproj]
warnings/Program.cs(7,34): warning CS8602: Dereference of a possibly null reference. [warnings/App.csproj]

Build succeeded.

warnings/Program.cs(15,19): warning CS8618: Non-nullable property 'City' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [warnings/App.csproj]
warnings/Program.cs(3,18): warning CS8600: Converting null literal or possible null value to non-nullable type. [warnings/App.csproj]
warnings/Program.cs(4,32): warning CS8602: Dereference of a possibly null reference. [warnings/App.csproj]
warnings/Program.cs(5,6): warning CS8625: Cannot convert null literal to non-nullable reference type. [warnings/App.csproj]
warnings/Program.cs(7,34): warning CS8602: Dereference of a possibly null reference. [warnings/App.csproj]
    5 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet bin/Debug/net10.0/App.dll
found has 16 characters
sending to nobody
City is null: True
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in warnings/Program.cs:line 7
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 warnings_session.py

The build succeeded with five warnings, and each one names a line:

Warning Line What the compiler saw
CS8600 3 Lookup returns string?, and its result went into a string
CS8602 4 and 7 .Length read on a value that may be null
CS8625 5 the literal null passed for a string parameter
CS8618 15 City is a string, and no constructor or initializer sets it

Then the program ran. Line 4 got lucky, because C-101 has an email address, and line 7, which CS8602 pointed at, threw NullReferenceException. The annotations changed nothing at run time: string and string? are both System.String, and the compiler adds no check. Everything nullable reference types do happens while the code compiles.

Key idea

string? declares that a reference may be null; the compiler then warns wherever a maybe-null value goes into a string or is read, and the program runs exactly as it would without the annotations.

Checkpoint

Check: string, string? and the warnings they raise

  1. Question 1 of 2 string? Lookup(string id) returns null for an unknown id. Which warning does string email = Lookup("C-999"); draw?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: CS8600, converting a possible null value to a non-nullable type

    An assignment of a maybe-null value to a non-nullable variable draws CS8600; the next read of that variable draws CS8602, as lines 3 and 7 did.

  2. Question 2 of 2 What does the compiler add to a method for its parameter string name, when nullable reference types are on?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing: the annotation is checked at compile time only

    Nullable reference types change the warnings, never the program: a caller that ignores the warnings can still pass null.

Flow analysis proves a value is not null before it is read

The compiler gives every expression a null-state: What the compiler knows about a reference at one point in the code: not-null or maybe-null. Assignments and null checks change it, and reading a maybe-null value draws warning CS8602., not-null or maybe-null, and updates it after each assignment and each null check. That tracking is flow analysis: The compiler's tracking of null-state through the statements of a method: it follows assignments, checks, patterns and early returns, but not the bodies of the methods a line calls.. checks/Program.cs reads the same lookups after a check, a pattern, ?? and required:

checks/Program.cs C# · checks/Program.cs
// The same lookups, written so that flow analysis can prove each value is not null where it is read.
string? found = Lookup("C-101");
if (found is null)
{
    return;
}

Console.WriteLine($"found has {found.Length} characters");

string? missing = Lookup("C-102");
Console.WriteLine(missing is not null ? $"missing has {missing.Length} characters" : "C-102 has no email");

string email = Lookup("C-102") ?? "no-reply@example.com";
Console.WriteLine($"C-102 is sent to {email}");

var customer = new Customer { City = "Pune" };
Console.WriteLine($"{customer.City} has {customer.City.Length} letters");

static string? Lookup(string id) => id == "C-101" ? "asha@example.com" : null;

class Customer
{
    public required string City { get; set; }
}
Building and running checks Python · checks_session.py
from session import run

# checks reads the same values after a null check, a pattern, ?? and required: it builds without a warning.
run("""
cd checks
dotnet build -v q
dotnet bin/Debug/net10.0/App.dll
""")

Output

$ cd checks
$ dotnet build -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet bin/Debug/net10.0/App.dll
found has 16 characters
C-102 has no email
C-102 is sent to no-reply@example.com
Pune has 4 letters

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checks_session.py

No warnings. Step through the null-state of each value:

Interactive · stage by stage The null-state of each value in checks Step through checks/Program.cs and watch the compiler's two states, maybe-null and not-null, change.
The null-state of each value in checks, step 6 of 6. Showing found = Lookup("C-101") (found: maybe-null), found is null: return (null path leaves), found.Length (found: not-null), missing is not null ? (not-null in the ? branch), Lookup(…) ?? fallback (email: not-null), required City (City: not-null). In focus: required City.found = Lookup("C-101")found: maybe-nullfound is null: returnnull path leavesfound.Lengthfound: not-nullmissing is not null ?not-null in the ? branchLookup(…) ?? fallbackemail: not-nullrequired CityCity: not-null

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. maybe-null. Lookup returns string?, so found starts as maybe-null. A read of found.Length here would draw CS8602.
  2. A check that leaves. if (found is null) return; ends the program on the null path. Every statement after it runs only when found has a value.
  3. not-null. After the check the compiler knows found is not-null, so found.Length draws no warning. The run printed found has 16 characters.
  4. One branch at a time. missing is maybe-null again, because C-102 has no email. In missing is not null ? … : …, the first branch sees not-null; the run took the other one and printed C-102 has no email.
  5. A default. ?? "no-reply@example.com" gives a string that is never null, so email can be a plain string and is not-null from the start.
  6. required. City is required, so every object initializer must set it, and the compiler treats it as not-null: no CS8618, and Pune has 4 letters.

A check that leaves the method, if (found is null) return;, proves found for every line after it. Inside missing is not null ? … : … only the first branch reads missing. ?? gives a value that is never null, and required makes every object initializer set City, which removes CS8618. Microsoft’s guide states the limit: the analysis does not trace into the bodies of methods, so a check hidden inside a helper proves nothing at the call site.

Making the warning go away by changing the type

Turning string missing into string? missing removes CS8600, and the warning moves to the next read of missing. That is progress only if the read gets a check: a ? says that the value may be missing, and the line that reads it must then handle that.

Checkpoint

Check: Null-state and flow analysis

  1. Question 1 of 2 Find returns string?. Does the last line draw a warning?

    Read the code, then choose one answer.

    string? name = Find(id);
    if (name is null)
    {
        Console.WriteLine("not found");
    }
    
    Console.WriteLine(name.Length);
    Show the answer: Question 1 of 2

    Answer: Yes, CS8602: the check does not leave, so after it name may still be null

    Flow analysis merges the two paths after the if: on one of them name is null, so the read is maybe-null. Add return to the null branch, or move the read into an else.

  2. Question 2 of 2 In missing is not null ? missing.Length : 0, where does the compiler treat missing as not-null?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In the first branch only

    A condition gives each branch its own null-state, which is why checks/Program.cs printed C-102 has no email without a warning.

Three tools go beyond these checks. The null-forgiving operator: A ! written after an expression, as in name!: it tells the compiler to treat the expression as not null and removes its nullable warnings. It changes nothing when the program runs. ! tells the compiler that a value is not null, and changes nothing when the program runs. In generic code, T? and the notnull constraint say whether a type argument may be null. #nullable directives and WarningsAsErrors decide where the checks apply and whether a nullable warning stops the build.

The ! operator and the nulls flow analysis cannot see, reproduced Pro: opens with a Pro, Premium or Ultimate pass

A build without warnings proves less than it seems: ! removes a warning without removing the null, and some nulls never draw a warning at all. Each case below was built and run on the .NET 10 SDK.

In this part:

  • The null-forgiving operator removes the warning and keeps the crash
  • Four nulls that draw no warning, measured
  • When the null-forgiving operator is the right tool: a decision table

4 runnable examples (C#, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Generic code: what T? means for int and for string, measured Pro: opens with a Pro, Premium or Ultimate pass

T may turn out to be a class or a struct, so T? cannot mean the same thing for both. The run below shows the case that catches callers out: a missing value that comes back as 0.

In this part:

  • T? on an unconstrained T gives 0 for int, reproduced
  • notnull, measured on a registry and on Dictionary

3 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Stricter builds: nullable warnings as errors, file by file Pro: opens with a Pro, Premium or Ultimate pass

A warning that the build accepts can be ignored, and an error cannot. This part makes nullable warnings stop the build, and shows how one file can stay outside the checks while the rest of the project is strict.

In this part:

  • WarningsAsErrors set to nullable, measured
  • Nullable contexts: two switches per file
  • Coming from Java: annotations against Optional

4 runnable examples (C#, CSPROJ, Python) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: an address book that builds with warnings as errors

addressbook treats every nullable warning as an error, and its build fails with four. The comment at the top of Program.cs lists the four lines it prints once it builds:

addressbook/App.csproj CSPROJ · addressbook/App.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>nullable</WarningsAsErrors>
  </PropertyGroup>

</Project>
addressbook/Program.cs C# · addressbook/Program.cs
// Practice: make this project build with nullable warnings as errors. Change types, add checks, and use ! at most
// once, with a comment that says why it is safe. When it builds, dotnet run prints these four lines:
//   Asha Rao: asha@example.com, 0 characters
//   Ravi Iyer: no email, 11 characters
//   Meena Das: not in the book
//   last search: Meena Das
var book = new AddressBook();
book.Add(new Contact("Asha Rao", "asha@example.com", null));
book.Add(new Contact("Ravi Iyer", null, "98200 11122"));

foreach (string name in new[] { "Asha Rao", "Ravi Iyer", "Meena Das" })
{
    Contact contact = book.Find(name);
    Console.WriteLine($"{name}: {contact.Email.ToLowerInvariant()}, {contact.Phone.Length} characters");
}

Console.WriteLine($"last search: {book.LastSearch}");

class Contact
{
    public Contact(string name, string email, string phone)
    {
        Name = name;
        Email = email;
        Phone = phone;
    }

    public string Name { get; }

    public string Email { get; }

    public string Phone { get; }
}

class AddressBook
{
    private readonly List<Contact> _contacts = [];

    public string LastSearch { get; private set; }

    public void Add(Contact contact) => _contacts.Add(contact);

    public Contact Find(string name)
    {
        LastSearch = name;
        foreach (Contact contact in _contacts)
        {
            if (contact.Name == name)
            {
                return contact;
            }
        }

        return null;
    }
}
Building addressbook Python · addressbook_session.py
from session import run

# The practice project as it starts: built with nullable warnings as errors. The compiler reports its errors in the
# order it finds them, which can change from one build to the next, so they are sorted, each shown once.
run("""
cd addressbook
dotnet build -v q | grep -E "error CS|Error\\(s\\)" | sort -u
""")

Output

$ cd addressbook
$ dotnet build -v q | grep -E "error CS|Error\(s\)" | sort -u
    4 Error(s)
addressbook/Program.cs(39,19): error CS8618: Non-nullable property 'LastSearch' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [addressbook/App.csproj]
addressbook/Program.cs(54,16): error CS8603: Possible null reference return. [addressbook/App.csproj]
addressbook/Program.cs(8,54): error CS8625: Cannot convert null literal to non-nullable reference type. [addressbook/App.csproj]
addressbook/Program.cs(9,35): error CS8625: Cannot convert null literal to non-nullable reference type. [addressbook/App.csproj]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 addressbook_session.py

Make addressbook build

Copy the addressbook folder. Decide which values may really be missing and give them ?, initialize LastSearch, let Find return Contact?, and check its result before reading it. Each change moves a warning to the lines that use the value, so build after each one. Run dotnet build until it reports 0 errors, then dotnet run.

Then take the self-check on the first two sections, string? and flow analysis: it starts with whichever of them you have answered less well so far.

Practice that adapts to you

Practise this lesson

7 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 From which .NET version on do new project templates set <Nullable>enable</Nullable>?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: .NET 6

    Projects created from .NET 6 templates or later have nullable reference types on; older projects need the setting added.

  2. Question 2 of 7 string a = "Pune"; string? b = "Pune"; What does a.GetType() == b.GetType() give?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: true: both are System.String

    Nullable reference types add no run-time type, which is also why typeof(string?) is refused with CS8639.

  3. Question 3 of 7 public string City { get; set; } draws CS8618. Which fix keeps City non-nullable and makes every caller give it a value?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: public required string City { get; set; }

    For a value every object must have, choose required or a constructor parameter; a ? is right only when the value may really be missing.

  4. Question 4 of 7 Which null-states does flow analysis give an expression?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: not-null and maybe-null

    A declaration says what a variable may hold; its null-state says what the compiler knows about it on a given line.

  5. Question 5 of 7 Email is declared string?. Does the read inside the if draw a warning?

    Read the code, then choose one answer.

    static bool HasEmail(Customer c) => c.Email is not null;
    
    if (HasEmail(customer))
    {
        Console.WriteLine(customer.Email.Length);
    }
    Show the answer: Question 5 of 7

    Answer: Yes, CS8602: the analysis does not look inside HasEmail

    Write the check inline, or annotate the helper so that its true result promises an email; the next lesson shows the attribute.

  6. Question 6 of 7 string email = Lookup(id) ?? "no-reply@example.com"; What is the null-state of email on the next line?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: not-null

    ?? with a value that is never null gives a value that is never null, which is why email could be a plain string.

  7. Question 7 of 7 After if (found is null) return; the code runs found = Lookup("C-102"); and then reads found.Length. What does the build report?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: CS8602 at the read: the new assignment made found maybe-null again

    Flow analysis follows the code in order: each assignment replaces what the last check proved.

JSON to C# Class Generate C# classes from JSON with nulls in it, and compare which properties it declares with ? against the warnings of this lesson.

Interview questions

Fresher · What is the difference between string and string? in C#?

Both are System.String when the program runs: a variable of either type can hold null, and nothing checks it at run time. The difference is a promise that the compiler checks. With nullable reference types on, string promises that the value is never null, so the compiler warns when a value that may be null is assigned to it (CS8600) or when a non-nullable property is left unset (CS8618). string? says the value may be missing, so the compiler warns when it is read without a check (CS8602). The annotations cost nothing at run time, and they turn the question “can this be null?” into a warning at the line where it matters.

  1. Mid to senior

    Nullable reference types are on and the build has no warnings, yet a nightly export crashes with NullReferenceException on customer.Email.Trim(). Email is declared string, and customers are read from a JSON file. How did the null get there, how do you fix the crash, and how do you stop it happening again?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A pull request turns nullable warnings into errors in a large project and makes the build pass by adding the ! operator in 140 places. Would you merge it as it is? How would you check those changes, and what would you ask for instead?

    Model answer, Pro. First step and follow-ups

  3. Mid

    A generic method T? Find<T>(IEnumerable<T> items, Func<T, bool> match) returns default when nothing matches. For stock counts, callers cannot tell "not found" from a real count of 0. Explain why T? did not help, and design a version that works for value types and reference types.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • With nullable reference types on, string means never null and string? means may be null; the compiler warns where the two meet, and the program runs exactly as it would without the annotations.
  • CS8600, CS8602, CS8618 and CS8625 point at an assignment, a read, an unset member and a null literal; each marks a line where a null can arrive or be read.
  • Flow analysis tracks maybe-null and not-null through assignments, checks, patterns, ?? and required, but not into the bodies of the methods a line calls.

Terms in this lesson

flow analysis
The compiler's tracking of null-state through the statements of a method: it follows assignments, checks, patterns and early returns, but not the bodies of the methods a line calls.
null-forgiving operator
A ! written after an expression, as in name!: it tells the compiler to treat the expression as not null and removes its nullable warnings. It changes nothing when the program runs.
null-state
What the compiler knows about a reference at one point in the code: not-null or maybe-null. Assignments and null checks change it, and reading a maybe-null value draws warning CS8602.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.