Skip to main content

C# Nullable Reference Types: How to Enable & Use Them

Learn C# nullable reference types in this complete guide. Enable them, fix warnings, and write null-safe code with practical examples. Start coding safer today!

If you have written C# for any length of time, you have met the most infamous exception in .NET: the dreaded NullReferenceException. Tony Hoare, the inventor of the null reference, famously called it his "billion-dollar mistake." The good news is that modern C# gives you a powerful tool to eliminate this entire class of bugs at compile time. In this guide to C# nullable reference types, you will learn exactly what they are, how to enable them, and how to use them effectively in real-world projects.

Whether you are a beginner trying to understand why your code throws null errors, an intermediate developer looking for null safety in C# best practices, or a senior engineer migrating a large legacy codebase, this tutorial covers everything you need.

What Are C# Nullable Reference Types?

Introduced in C# 8.0 (and the default in new .NET projects today), C# nullable reference types are a compiler feature that helps you express your intent about whether a reference variable is allowed to be null. Crucially, this is a static analysis feature. It does not change runtime behavior or add overhead — instead, the compiler reads your annotations and warns you when you might dereference a null value.

Before this feature, every reference type (like string, object, or your own classes) could silently hold null. There was no way to tell the compiler "this string should never be null." Now you can be explicit:

string name = "Alice";      // Non-nullable: must never be null
string? middleName = null;   // Nullable: null is explicitly allowed

// The compiler warns you here, because middleName could be null:
Console.WriteLine(middleName.Length); // Warning CS8602: Dereference of a possibly null reference

The ? suffix is the key. string means "non-nullable reference type," while string? means "nullable reference type." This mirrors the long-standing syntax for nullable value types like int?.

How to Enable Nullable Reference Types in C#

By default, projects created with .NET 6 and later have this feature turned on. But if you are working with an older project, you need to enable it manually. There are two main ways to do this.

1. Enable Nullable Reference Types Project-Wide

The recommended approach is to set the nullable annotation context in your .csproj file. This applies the feature to every file in the project:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

The <Nullable>enable</Nullable> setting is what activates null safety across your codebase.

2. Enable It Per-File With a Directive

For gradual migration of a large legacy project, enabling the feature everywhere at once can produce thousands of warnings. Instead, you can opt in one file at a time using a preprocessor directive at the top of the file:

#nullable enable

namespace MyApp.Services;

public class UserService
{
    public string GetUserName(int id)
    {
        // This method now participates in null safety analysis
        return "Alice";
    }
}

You can also use #nullable disable to turn it off, or #nullable restore to revert to the project default. This granular control is the single most important tool for migrating real-world applications without being buried in warnings.

Understanding the Compiler Warnings

Once enabled, the compiler analyzes your code and produces warnings when null safety might be violated. The two you will see most often are:

  • CS8602 — Dereference of a possibly null reference. You tried to access a member on something that could be null.
  • CS8625 — Cannot convert null literal to non-nullable reference type. You tried to assign null to a variable that promised never to be null.
  • CS8618 — Non-nullable field/property must contain a non-null value when exiting the constructor.

Here is a classic example that triggers CS8618, a warning many developers hit on day one:

public class Customer
{
    // Warning CS8618: Non-nullable property 'Name' must contain a non-null value
    public string Name { get; set; }
}

The compiler is telling you that Name claims to never be null, but nothing guarantees it gets initialized. You have several clean ways to fix this, which brings us to best practices.

C# Null Check Best Practices With Nullable Reference Types

Initialize Non-Nullable Properties Properly

To satisfy the compiler, give the property a value. The required keyword (C# 11+) is the modern, idiomatic solution because it forces callers to set the property during initialization:

public class Customer
{
    public required string Name { get; set; }
    public string? MiddleName { get; set; }   // Genuinely optional
}

// The compiler now enforces this at the call site:
var customer = new Customer { Name = "Alice" }; // OK
// var bad = new Customer();                    // Error: 'Name' must be set

Use Null Checks to "Narrow" the Type

The compiler is smart. Once you check for null, it understands that the variable is safe to use afterward — a feature called flow analysis. This is the heart of null safety in C#:

public int GetNameLength(string? name)
{
    if (name is null)
    {
        return 0;
    }

    // No warning here! The compiler knows 'name' cannot be null past the guard.
    return name.Length;
}

Prefer is null and is not null over == null. The pattern-matching form cannot be fooled by an overloaded == operator, making it more reliable.

Leverage Null-Conditional and Null-Coalescing Operators

C# offers concise operators that work beautifully with nullable reference types:

string? input = GetUserInput();

// Null-conditional (?.): returns null instead of throwing if input is null
int? length = input?.Length;

// Null-coalescing (??): provide a fallback value
string safeValue = input ?? "default";

// Null-coalescing assignment (??=): assign only if currently null
input ??= "assigned because it was null";

Use the Null-Forgiving Operator Sparingly

Sometimes you know more than the compiler does. The null-forgiving operator (!) tells the compiler "trust me, this is not null." It suppresses the warning but provides zero runtime protection:

public string GetConfig(IDictionary<string, string> settings)
{
    // You are certain the key exists, so you override the warning:
    return settings["ApiKey"]!;
}

Pitfall: Overusing ! defeats the entire purpose of the feature. If you find yourself sprinkling it everywhere, your annotations are probably wrong. Treat each ! as a small piece of technical debt and a promise you must keep.

Advanced Nullable Reference Types in C#: Attributes

For library authors and senior developers, .NET provides nullability attributes in System.Diagnostics.CodeAnalysis that let you express subtle contracts the basic syntax cannot. These are essential for building APIs that behave correctly under null safety analysis.

using System.Diagnostics.CodeAnalysis;

public class StringHelper
{
    // NotNullWhen: if this returns true, 'result' is guaranteed non-null
    public bool TryGetValue(string? input, [NotNullWhen(true)] out string? result)
    {
        if (!string.IsNullOrEmpty(input))
        {
            result = input;
            return true;
        }

        result = null;
        return false;
    }
}

// At the call site, the compiler understands the contract:
if (helper.TryGetValue(raw, out var value))
{
    Console.WriteLine(value.Length); // No warning — value is known non-null
}

Other useful attributes include [MaybeNull], [AllowNull], [DisallowNull], and [NotNullIfNotNull]. They let you precisely document the null behavior of your public surface, which is invaluable when others consume your library.

Common Pitfalls When Migrating to Nullable Reference Types

  • Treating warnings as errors too early. When migrating a big codebase, enabling <WarningsAsErrors> for nullability immediately can block your build. Migrate file by file with #nullable enable first.
  • Forgetting third-party libraries. Older NuGet packages may not be annotated. Their APIs appear as "null-oblivious," meaning the compiler stays silent. Verify their actual behavior yourself.
  • Assuming runtime protection. Nullable reference types are compile-time only. External data — JSON, databases, reflection — can still slip a null past the compiler. Always validate data crossing your application boundary.
  • Over-annotating with ?. If almost everything is nullable, you lose the signal. Make non-nullable the default and only mark something nullable when null is a genuine, meaningful state.

A Complete, Runnable Example

Let's tie it all together with a small program that demonstrates safe null handling end to end:

#nullable enable
using System;

public record Order(int Id, string? Notes);

public class OrderProcessor
{
    public string Describe(Order? order)
    {
        if (order is null)
        {
            return "No order provided.";
        }

        // 'order' is now non-null. 'Notes' may still be null, so we guard it.
        string notes = order.Notes ?? "(no notes)";
        return $"Order #{order.Id}: {notes}";
    }
}

public static class Program
{
    public static void Main()
    {
        var processor = new OrderProcessor();
        Console.WriteLine(processor.Describe(new Order(101, "Gift wrap")));
        Console.WriteLine(processor.Describe(new Order(102, null)));
        Console.WriteLine(processor.Describe(null));
    }
}

This code compiles with zero nullable warnings because every potential null is handled explicitly — exactly the discipline the feature is designed to encourage.

Conclusion: Key Takeaways

Mastering C# nullable reference types is one of the highest-leverage skills you can develop for writing robust, maintainable .NET applications. By making nullability explicit, you push an entire category of runtime crashes into compile-time warnings you can fix before your users ever see them.

Here are the key takeaways:

  • Enable nullable reference types with <Nullable>enable</Nullable> in your .csproj, or use #nullable enable for gradual migration.
  • Use string for values that must never be null and string? when null is a valid state.
  • Follow C# null check best practices: prefer is null, use the null-conditional (?.) and null-coalescing (??) operators, and lean on the compiler's flow analysis.
  • Use the null-forgiving operator (!) only when you are certain — and treat it as a deliberate exception.
  • Remember that null safety in C# is enforced at compile time, so always validate external data at runtime boundaries.

Turn the feature on in your next project, work through the warnings methodically, and you will quickly find your code becomes clearer, safer, and far less prone to the billion-dollar mistake. Happy coding!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...