Nullable Reference Types

Vaibhav • September 11, 2025

In earlier chapters, we explored value types like int, bool, and DateTime, and how they can be made nullable using int? or bool?. But reference types - like string, object, or custom classes - have always been nullable by default. That flexibility comes with risk: null reference exceptions are one of the most common runtime errors in C#.

Starting with C# 8.0, the language introduced nullable reference types as a way to make nullability explicit and safer. This article walks through what they are, how to enable them, how they change your code, and how to use them effectively to avoid bugs and clarify intent.

What are Nullable Reference Types?

Nullable reference types are not a new kind of type - they’re a language feature that changes how the compiler interprets your code. When enabled, the compiler distinguishes between:

  • string - a non-nullable reference type (should never be null)
  • string? - a nullable reference type (may be null)

This distinction allows the compiler to warn you when you might be dereferencing a null value, or assigning null to a non-nullable variable.

Nullable reference types are a compile-time feature. They don’t change runtime behavior - they help you catch potential null issues before your code runs.

Enabling Nullable Reference Types

To use nullable reference types, you need to enable them in your project. You can do this in your .csproj file:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

Or you can enable it per file using a directive:

#nullable enable

Once enabled, the compiler starts analyzing your code for null safety based on your annotations.

Declaring Nullable and Non-Nullable References

With nullable reference types enabled, you must be explicit about whether a reference can be null:

string name = "Vaibhav";      // Non-nullable
string? nickname = null;     // Nullable

If you try to assign null to a non-nullable reference, the compiler will warn you:

string name = null; // ⚠️ Warning: possible null assignment

Nullability Warnings and Flow Analysis

The compiler uses flow analysis to track whether a variable might be null at a given point. Consider this example:

string? message = GetMessage();

Console.WriteLine(message.Length); // ⚠️ Warning: possible null dereference

Since message is nullable, accessing Length directly is risky. You can fix this by checking for null:

if (message != null)
{
    Console.WriteLine(message.Length); // ✅ Safe
}

Note: The compiler tracks null checks and adjusts its warnings accordingly. This helps you write safer code without needing runtime checks everywhere.

Using the Null-Forgiving Operator (!)

Sometimes you know a value isn’t null, even if the compiler isn’t sure. You can use the null-forgiving operator ! to suppress the warning:

string? title = GetTitle();
Console.WriteLine(title!.Length); // Tells compiler: "trust me, it's not null"

Use this sparingly - it’s a way to silence the compiler, but if you’re wrong, you’ll still get a runtime exception.

Prefer null checks over using !. Only use the null-forgiving operator when you’re absolutely sure the value is not null.

Nullable Parameters and Return Types

You can annotate method parameters and return types to indicate nullability:

string? FindUser(string? username)
{
    if (username == null) return null;
    return "User: " + username;
}

This makes your method’s contract clear: it accepts a nullable input and may return a nullable result.

Nullability in Fields and Properties

Class fields and properties also benefit from nullability annotations. Consider this class:

class Person
{
    public string Name { get; set; } // Must be non-null
    public string? Nickname { get; set; } // May be null
}

If you forget to initialize Name, the compiler will warn you. You can fix it with a constructor or default value.

Suppressing Warnings with #nullable directives

You can temporarily disable nullable warnings in a file or region using directives:

#nullable disable
string risky = null; // No warning

#nullable restore
string safe = null; // ⚠️ Warning again

This is useful when migrating legacy code or integrating third-party libraries.

Interoperability with Legacy Code

Nullable reference types are opt-in. If you’re working with older code that doesn’t use them, the compiler assumes all reference types are nullable. You can annotate external APIs using #nullable directives or attributes like [MaybeNull] and [NotNull].

Nullable reference types don’t change the runtime behavior of your program. They only affect compiler warnings and static analysis.

Summary

Nullable reference types help you write safer, clearer code by making nullability explicit. With string vs string?, the compiler can warn you about risky assignments and dereferences. You can enable the feature project-wide or per file, annotate variables, parameters, and return types, and use flow analysis to guide safe usage. The null-forgiving operator ! lets you override warnings when needed, but should be used cautiously.

By adopting nullable reference types, you reduce the risk of null reference exceptions and make your code’s intent more obvious to both the compiler and other developers. In the next article, we’ll explore Pattern Matching - a powerful feature that lets you write cleaner, more expressive conditionals using modern C# syntax.