C# Null-Coalescing Operators (?? and ??=)

The null-coalescing operators help you write clear fallback logic when a value may be null. The ?? operator chooses the left value when it exists, otherwise it evaluates and returns the right value. The ??= operator assigns a fallback only when the target variable, property, or indexer is currently null.

Overview: How Null-Coalescing Works

In C#, null means a reference does not point to an object, or a nullable value type does not contain a value. Many programs need a default when data is missing: a display name, an empty collection, a configuration value, or a fallback calculation. You can write that with an if statement, but C# gives you purpose-built operators for the common cases.

The expression left ?? right means “use left unless it is null; otherwise use right.” It is a short-circuiting operator. If the left operand is not null, the right operand is not evaluated at all. That matters when the right side calls a method, allocates an object, logs something, or throws an exception.

The assignment form target ??= value means “if target is null, assign value to it, then produce the target’s value.” It is commonly used to initialize fields, local variables, dictionaries, lists, and cached values exactly when needed. If the target already has a value, the right side is skipped.

For reference types such as string?, List<T>?, or a custom class, the operator checks whether the reference is the null reference. For nullable value types such as int? or DateTime?, it checks the hidden HasValue flag of Nullable<T>. When an int? has a value, age ?? 0 produces an int, not another nullable integer, because the fallback is also an int.

Null-coalescing is not the same as checking for an empty string, zero, false, or an empty collection. Only null triggers the fallback. If name is "", then name ?? "Guest" returns the empty string. If count is 0, then count ?? 10 returns zero when count is an int? with the value 0.

Syntax

string? maybeValue = null;
string fallbackValue = "fallback";
string result = maybeValue ?? fallbackValue;

string? target = null;
target ??= fallbackValue;

string? firstChoice = null;
string? secondChoice = "second";
string finalChoice = "final";
string value = firstChoice ?? secondChoice ?? finalChoice;
Syntax Meaning
x ?? y Return x when it is not null; otherwise return y.
x ??= y Assign y to x only when x is currently null.
a ?? b ?? c Choose the first non-null value. The operator is right-associative, so this groups as a ?? (b ?? c).
x ?? throw ... Use x when present; otherwise throw an exception. This is useful for validating required values.

The left operand of ?? must be a type that can be null: a reference type, a nullable value type, a type parameter that is not known to be non-nullable, or a similar nullable-capable type. The left operand of ??= must be assignable, such as a variable, property, or indexer. You cannot write GetName() ??= "Guest" because a method call result is not a storage location.

Examples

Choosing a Display Fallback

#nullable enable
using System;

class Program
{
    static void Main()
    {
        string? nickname = null;
        string username = "alex42";

        string displayName = nickname ?? username;
        Console.WriteLine(displayName);

        nickname = "Lex";
        displayName = nickname ?? username;
        Console.WriteLine(displayName);
    }
}

Output:

alex42
Lex

The first expression uses username because nickname is null. After nickname is assigned "Lex", the same expression returns the nickname. Notice that this does not test whether the string is long, meaningful, or non-empty. It only tests whether the reference is null.

Using ?? with Nullable Value Types

using System;

class Program
{
    static void Main()
    {
        int? requestedPageSize = null;
        int pageSize = requestedPageSize ?? 25;
        Console.WriteLine($"Page size: {pageSize}");

        requestedPageSize = 0;
        pageSize = requestedPageSize ?? 25;
        Console.WriteLine($"Page size: {pageSize}");
    }
}

Output:

Page size: 25
Page size: 0

The variable requestedPageSize is an int?, so it can contain either an integer or no value. When it is null, ?? produces the fallback 25. When it contains 0, zero is returned because zero is a real value, not a missing value.

Lazy Initialization with ??=

#nullable enable
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string>? messages = null;

        messages ??= new List<string>();
        messages.Add("Saved draft");

        messages ??= new List<string>();
        messages.Add("Queued email");

        Console.WriteLine(messages.Count);
        Console.WriteLine(string.Join(", ", messages));
    }
}

Output:

2
Saved draft, Queued email

The first ??= creates the list because messages is null. The second ??= does nothing because the list already exists. This pattern is useful for local setup and lazy fields, but it should still be used where the object being initialized is allowed to be missing before that point.

Chaining Fallbacks and Throw Expressions

#nullable enable
using System;

class Program
{
    static string RequireConnectionString(string? commandLine, string? environment, string? file)
    {
        return commandLine ?? environment ?? file ?? throw new InvalidOperationException("Connection string is required.");
    }

    static void Main()
    {
        string? commandLine = null;
        string? environment = "Server=prod;Database=Store";
        string? file = "Server=local;Database=Store";

        string connection = RequireConnectionString(commandLine, environment, file);
        Console.WriteLine(connection);
    }
}

Output:

Server=prod;Database=Store

The method checks the possible sources in priority order. The command-line value is missing, so C# evaluates the environment value next. Because that value is present, the file value and the throw expression are not needed for the result.

How It Works Step by Step

  1. The compiler evaluates the left operand of ?? exactly once.
  2. If the left operand is not null, that value becomes the expression result and the right operand is skipped.
  3. If the left operand is null, the right operand is evaluated and becomes the result.
  4. For nullable value types, the check is based on whether HasValue is true. If there is a value, the underlying value is used.
  5. For ??=, the target storage location is evaluated, checked for null, and assigned only when needed.
  6. Both operators are right-associative, so a ?? b ?? c behaves like a ?? (b ?? c).

The generated code is similar to a careful temporary-variable check. For example, string result = GetName() ?? "Guest"; behaves as if the result of GetName() were stored once, tested once, and then either reused or replaced with "Guest". This avoids accidentally calling the left expression twice.

Common Mistakes

Expecting ?? to Handle Empty Strings

#nullable enable
using System;

class Program
{
    static void Main()
    {
        string? title = "";
        Console.WriteLine(title ?? "Untitled");
        Console.WriteLine(string.IsNullOrWhiteSpace(title) ? "Untitled" : title);
    }
}

Output:


Untitled

The first line prints the empty string because title is not null. If your rule treats null, empty, and whitespace as missing, use string.IsNullOrWhiteSpace or a helper method instead of ?? alone.

Putting Expensive Work on the Left Side

#nullable enable
using System;

class Program
{
    static string? FindCachedName()
    {
        Console.WriteLine("Checking cache");
        return null;
    }

    static string LoadName()
    {
        Console.WriteLine("Loading name");
        return "Mina";
    }

    static void Main()
    {
        string name = FindCachedName() ?? LoadName();
        Console.WriteLine(name);
    }
}

Output:

Checking cache
Loading name
Mina

Short-circuiting skips the right side when the left side has a value, but it never skips the left side. Put the cheap or already-available value on the left. Put the fallback calculation on the right only when it should run after the first choice is missing.

Using ??= on a Non-Assignable Expression

GetName() ??= "Guest";

This does not compile because ??= must assign back into a variable, property, or indexer. A method call returns a value, not a location that can receive an assignment.

#nullable enable
using System;

class Program
{
    static void Main()
    {
        string? name = null;
        name ??= "Guest";
        Console.WriteLine(name);
    }
}

Output:

Guest

Best Practices

  • Use ?? for simple fallback values where only null should trigger the fallback.
  • Use ??= for lazy initialization of nullable variables, fields, properties, or indexer entries.
  • Keep the left side of ?? as the preferred value and the right side as the fallback. This makes the code read in priority order.
  • Remember that ?? does not treat "", whitespace, 0, false, or empty collections as missing.
  • Use ?? throw when a value is required and continuing without it would hide a bug.
  • Avoid long chains when each fallback has complicated logic. An if statement or helper method can be clearer.
  • Be careful with shared mutable fields. ??= is convenient, but it is not a complete thread-safety strategy by itself.
  • With nullable reference types enabled, prefer ?? and ??= over the null-forgiving operator when you can provide a real fallback.

Practice Exercises

  1. Create a string? variable named preferredLanguage. Print it, or print "en" when it is null.
  2. Create a nullable List<int>. Use ??= before adding three numbers, then print the count.
  3. Write a method that accepts three nullable strings: a profile name, an account name, and an email address. Return the first non-null value, or throw an exception when all three are missing.

Summary

  • x ?? y returns x when x is not null; otherwise it returns y.
  • x ??= y assigns y only when x is currently null.
  • The right side of both operators is evaluated only when it is needed.
  • The operators work with nullable reference types and nullable value types.
  • Only null triggers the fallback. Empty strings, zero, false, and empty collections are still real values.
  • Use ?? throw to fail fast when a required value is missing.
  • Use these operators to make null-handling concise, but switch to explicit control flow when the rule is more complex than a simple fallback.