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
- The compiler evaluates the left operand of
??exactly once. - If the left operand is not
null, that value becomes the expression result and the right operand is skipped. - If the left operand is
null, the right operand is evaluated and becomes the result. - For nullable value types, the check is based on whether
HasValueis true. If there is a value, the underlying value is used. - For
??=, the target storage location is evaluated, checked fornull, and assigned only when needed. - Both operators are right-associative, so
a ?? b ?? cbehaves likea ?? (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 onlynullshould 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
?? throwwhen a value is required and continuing without it would hide a bug. - Avoid long chains when each fallback has complicated logic. An
ifstatement 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
- Create a
string?variable namedpreferredLanguage. Print it, or print"en"when it isnull. - Create a nullable
List<int>. Use??=before adding three numbers, then print the count. - 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 ?? yreturnsxwhenxis notnull; otherwise it returnsy.x ??= yassignsyonly whenxis currentlynull.- 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
nulltriggers the fallback. Empty strings, zero,false, and empty collections are still real values. - Use
?? throwto 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.
