Nullable Reference Types

Introduced in C# 8, nullable reference types let the compiler catch the most common bug in .NET — the dreaded NullReferenceException — before your program ever runs. By annotating types as string (never null) or string? (may be null), you give the compiler enough information to warn you exactly where a value might be null. It's a compile-time safety net, not a runtime change.

Learn Nullable Reference Types in our free C# course — an interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of a form at a doctor's office. Some fields are marked required (your name) and some are clearly optional (a middle name). If a required field is blank, the receptionist catches it at the desk — before you ever see the doctor. Nullable reference types are exactly that receptionist: string is a required field the compiler insists you fill, and string? is an optional one you must remember to check. The errors are caught up front, not in the middle of the appointment (at runtime).

Nullable reference types are annotations plus static analysis . When the feature is on, the ? on a reference type tells the compiler "this might be null", and the absence of ? says "this should never be null". The compiler then performs null-state flow analysis and warns you whenever you risk dereferencing a maybe-null value.

Crucially, none of this changes runtime behavior: at runtime string and string? are the same plain reference. This is purely a compile-time safety feature — distinct from int? , which really is a different runtime type.

1. Enabling NRTs: string vs string?

Turn the feature on with #nullable enable at the top of a file, or project-wide with in your .csproj . Once enabled, a plain string means "never null" and string? means "may be null". Read the worked example, run it, and notice which lines would warn.

2. Null-State Flow Analysis

The compiler doesn't just look at declarations — it follows the flow of your code. Before a null check a variable is "maybe-null"; after a guard like if (text == null) return; the compiler narrows it to "not-null" for the rest of the method. This is why a single well-placed check silences the warning.

Your turn. Annotate the parameter as nullable and add a ?? fallback so the method handles null safely. Fill in the two ___ blanks.

3. The Null Operators: ?. , ?? , ??=

These three operators are your everyday tools for working with maybe-null values. ?. (null-conditional) short-circuits to null instead of throwing; ?? (null-coalescing) supplies a fallback; and ??= (null-coalescing assignment) sets a value only when the target is currently null.

Now you try: use a guard so flow analysis narrows the value, then call a string method safely. Fill in the two ___ blanks.

4. The Null-Forgiving Operator !

Sometimes you know a value isn't null but the compiler can't prove it. The trailing ! (null-forgiving operator) tells the compiler "trust me, this isn't null" and suppresses the warning. Use it sparingly: it's a promise, not a runtime check, so a wrong promise still throws a NullReferenceException at runtime. Prefer a real check whenever you can.

5. Null-State Attributes

Attributes from System.Diagnostics.CodeAnalysis — like [NotNull] , [MaybeNull] and [NotNullWhen(true)] — describe the null-state after a method call, so flow analysis can reason about helpers such as the classic TryGet pattern. They let your APIs teach the compiler what they guarantee.

It's easy to assume string? and int? are the same idea, but they're fundamentally different. int? is genuine syntax for the runtime type Nullable int — a real struct with HasValue and Value members that occupies different memory than a plain int .

string? , on the other hand, is only a compile-time annotation. At runtime there is no separate "nullable string" type — it's the same string reference, which could always hold null anyway. The ? exists purely so the compiler can warn you:

So nullable value types add runtime capability; nullable reference types add compile-time warnings . Same ? symbol, very different machinery.

Here's the pattern you'll use constantly in real apps: required fields are non-nullable, optional ones are string? , and the rendering code uses ?. and ?? to handle the optional values cleanly. The annotations document your intent and get checked by the compiler.

Notice how the model itself documents which data is guaranteed and which is optional — the type signatures are the documentation.

Q: Do nullable reference types stop null at runtime?

No. They're a compile-time feature. At runtime string and string? are the same reference and either can technically be null. NRTs just give you warnings so you fix the risks before shipping.

int? is a real runtime type, Nullable int , with HasValue / Value . string? is only a compile-time annotation on a reference type that could already hold null — there's no separate runtime type.

Occasionally — for example after a check the compiler can't follow, or in tests. But it's a promise, not a guard: if you're wrong, you get a NullReferenceException . Prefer a real check or pattern match.

Q: How do I add NRTs to a big existing project?

Incrementally. Turn it on file by file with #nullable enable (or per folder), fix that file's warnings, then move on — rather than enabling it everywhere and facing thousands of warnings at once.

No blanks this time — just a brief and an outline. Write a DisplayName(string? first, string? last) that returns "Anonymous" when both parts are missing, otherwise joins the available parts with a space using ?? and Trim() . Run it and check your output against the expected lines in the comments.

Practice quiz

What is the main purpose of nullable reference types (NRTs) in C# 8 and later?

  • To give compile-time warnings about possible null dereferences
  • To make programs run faster at runtime
  • To replace nullable value types like int?
  • To stop null from existing at runtime entirely

Answer: To give compile-time warnings about possible null dereferences. NRTs are a compile-time feature: the compiler tracks null-state and warns where you might dereference null. They do not change runtime behavior or remove null.

With nullable reference types enabled, what does plain string (no question mark) express?

  • The same as object
  • A string that may be null
  • A non-nullable string the compiler expects to never be null
  • A value type

Answer: A non-nullable string the compiler expects to never be null. Under #nullable enable, string is the non-nullable annotation: the compiler warns if you try to assign null to it. Use string? to opt into possible null.

How do you turn the nullable reference types feature on for a whole project?

  • Call Enable() at startup
  • Set <Nullable>enable</Nullable> in the .csproj (or #nullable enable per file)
  • Decorate Main with an attribute
  • Add a using directive

Answer: Set <Nullable>enable</Nullable> in the .csproj (or #nullable enable per file). Project-wide, you set <Nullable>enable</Nullable> in the csproj. Per file you can use the #nullable enable directive.

What does the null-forgiving operator (the trailing !) do?

  • Throws if the value is null
  • Converts a value type to a reference type
  • Marks a parameter as required
  • Tells the compiler to suppress its null warning for that expression

Answer: Tells the compiler to suppress its null warning for that expression. The null-forgiving operator (x!) suppresses the compiler's null warning. It is a promise from you, not a runtime check, so it can hide a real NullReferenceException.

What is null-state flow analysis?

  • The compiler tracking whether a variable is maybe-null or not-null as code flows
  • A way to log nulls at runtime
  • A LINQ operator
  • A runtime garbage-collection pass

Answer: The compiler tracking whether a variable is maybe-null or not-null as code flows. The compiler follows the flow of your code; after a check like if (x != null) it knows x is not-null inside that branch and adjusts its warnings accordingly.

Which warning typically signals 'dereference of a possibly null reference'?

  • CS1002
  • CS8602
  • CS0001
  • CS0103

Answer: CS8602. CS8602 is the classic 'dereference of a possibly null reference' warning. CS8600 covers converting null (or a maybe-null) into a non-nullable type.

What does the ??= operator do?

  • Always overwrites the left side
  • Compares two values for null
  • Throws when the left side is null
  • Assigns the right side only if the left side is currently null

Answer: Assigns the right side only if the left side is currently null. The null-coalescing assignment ??= assigns the right-hand value only when the left-hand variable is null, a concise way to set defaults.

How are nullable reference types different from nullable value types like int?

  • int? is compile-time only
  • NRTs only work on int
  • NRTs are compile-time annotations on reference types; int? is a real runtime Nullable<int> struct
  • They are exactly the same feature

Answer: NRTs are compile-time annotations on reference types; int? is a real runtime Nullable<int> struct. int? is the runtime type Nullable<int>, a real struct. NRTs are compile-time annotations and analysis on reference types and erase to plain references at runtime.

What does the [NotNull] attribute on an out parameter communicate to the compiler?

  • The parameter must always be passed null
  • When the method returns, that value is guaranteed not null
  • The method never returns
  • The parameter is ignored

Answer: When the method returns, that value is guaranteed not null. Post-condition attributes like [NotNull] (and [MaybeNull]) describe the null-state after a call, helping flow analysis reason about helper methods such as TryGet patterns.

A pragmatic strategy for migrating a large codebase to nullable reference types is to:

  • Enable it file by file with #nullable enable and address warnings incrementally
  • Replace every string with string?
  • Enable it everywhere and fix every warning in one commit
  • Never enable it

Answer: Enable it file by file with #nullable enable and address warnings incrementally. Incremental adoption, enabling NRTs file by file (or per-folder) and resolving warnings gradually, keeps the migration manageable rather than facing thousands of warnings at once.