Why null handling comes up in every C# interview
Tony Hoare called null "my billion-dollar mistake." In C#, null has evolved from a
simple runtime hazard (NullReferenceException) into a first-class part of the type
system with C# 8's nullable reference types. Interviewers probe null handling because
it directly relates to code correctness, API clarity, and defensive programming. They
want to see you distinguish between "this value is optional" and "this should never be
null," use the right operator for each case, and understand what C# 8's nullable
annotations actually do at runtime.
Two completely different kinds of null in C#
C# has two orthogonal null systems that are easy to confuse:
Nullable value types (int?) | Nullable reference types (string?) | |
|---|---|---|
| Introduced | C# 2.0 | C# 8.0 |
| Runtime effect | Real — Nullable<T> struct exists | None — compile-time annotation only |
| Runtime type | System.Nullable<int> | System.String (unchanged) |
| Opt-in required | No — always available | Yes — <Nullable>enable</Nullable> |
Understanding this distinction is crucial. int? is a genuine runtime type that
changes how the CLR represents the value. string? is purely a compile-time hint —
at runtime it is indistinguishable from string.
Nullable value types — Nullable<T>
All C# value types (int, double, bool, DateTime, struct) are non-nullable
by default — they cannot be set to null. Nullable<T> (written as T?) wraps a
value type and adds a HasValue flag.
// int — can only hold numeric values
int a = 42;
// a = null; // CS0037 — int cannot be null
// int? — can hold null OR numeric values
int? b = 42;
int? c = null;
// The two properties:
Console.WriteLine(b.HasValue); // True
Console.WriteLine(b.Value); // 42
Console.WriteLine(c.HasValue); // False
// Console.WriteLine(c.Value); // InvalidOperationException!
Console.WriteLine(c.GetValueOrDefault()); // 0 — safe, no throw
Console.WriteLine(c.GetValueOrDefault(-1)); // -1 — custom fallback
At runtime, Nullable<int> is a struct containing bool HasValue and int Value.
The compiler synthesises special handling: int? x = null compiles to
Nullable<int> x = new Nullable<int>() (where HasValue = false).
Where nullable value types are essential
Database columns that allow NULL are the primary use case:
// Entity Framework model:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public DateTime? DiscontinuedDate { get; set; } // null = still active
public decimal? Weight { get; set; } // null = not measured
}
// Querying:
var active = db.Products.Where(p => p.DiscontinuedDate == null).ToList();
Nullable value types also appear in: optional form fields, configuration values with
defaults, three-state booleans (bool? for true/false/unknown), and nullable method
return values when "no result" is a valid state distinct from any value.
The null-coalescing operator ??
?? returns the left operand if non-null, otherwise the right operand. It short-circuits
— the right side is not evaluated if the left is non-null.
string? name = GetName(); // may return null
// Verbose form:
string display = name != null ? name : "Guest";
// With ??:
string display = name ?? "Guest";
// Chain for fallback hierarchy:
string value = GetFromCache()
?? GetFromDatabase()
?? GetFromConfig()
?? "default";
// Nullable value type:
int? optionalCount = GetCount(); // may be null
int count = optionalCount ?? 0;
// Lazy field initialisation:
private string? _label;
public string Label => _label ?? (_label = ComputeLabel()); // initialise on first access
Null-coalescing assignment ??=
??= assigns the right side to the variable only if the variable is currently null.
// Without ??=:
if (_cache == null)
_cache = new Dictionary<string, object>();
// With ??= (C# 8+):
_cache ??= new Dictionary<string, object>();
// Property initialisation:
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
// First access creates the list; subsequent accesses return it
The null-conditional operator ?.
?. navigates a chain of member accesses, returning null at the first null link
instead of throwing NullReferenceException. It converts the result to a nullable
type when the final member is a value type.
User? user = GetUser(id); // may return null
// Without ?.: three levels of null checking
string? city = null;
if (user != null && user.Address != null)
city = user.Address.City;
// With ?.:
string? city = user?.Address?.City; // null if user or Address is null
// With value types — result becomes nullable:
int? orderCount = user?.Orders?.Count; // int? because Count is int
// Safe event invocation — the classic use case:
EventHandler? OnChanged;
OnChanged?.Invoke(this, EventArgs.Empty); // skip if no subscribers
// Index notation:
string? firstOrderItem = user?.Orders?[0]?.Description;
// Combine with ??: provide a default
string display = user?.Name ?? "Guest";
int count = user?.Orders?.Count ?? 0;
The ?. chain short-circuits: once a null is encountered, the entire expression
evaluates to null without executing further member accesses or method calls.
Nullable reference types — C# 8
Before C# 8, string could be null at any time — the type system provided no way to
distinguish "this string is always set" from "this string might be absent." C# 8
introduced a compile-time annotation layer (not a runtime change) that lets you
express this intent.
#nullable enable
// Non-nullable — must always be a valid string:
string name = "Alice"; // fine
// name = null; // CS8600: cannot assign null to non-nullable string
// Nullable — can be null:
string? nickname = null; // fine
Console.WriteLine(nickname.Length); // CS8602: dereference of possibly null reference
// Safe access patterns the compiler accepts:
if (nickname != null)
Console.WriteLine(nickname.Length); // flow analysis narrows to non-null
Console.WriteLine(nickname?.Length); // null-conditional returns null safely
Console.WriteLine(nickname?.Length ?? 0); // with default
Enabling nullable reference types
<!-- Project-wide in .csproj — recommended for all new projects -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// Per-file (incremental migration of existing code):
#nullable enable // enable for remainder of this file
#nullable disable // disable (for legacy code you haven't migrated yet)
#nullable restore // restore to project default
Annotations only (#nullable enable annotations) enables the annotation syntax
without emitting warnings — useful for marking up APIs before you are ready to fix
all call sites.
Flow analysis
The C# compiler performs null-state flow analysis through branches, null checks, and pattern matching:
#nullable enable
void Process(string? value)
{
// value is "maybe null" here
Console.WriteLine(value.Length); // warning
if (value == null)
return;
// value is "not null" here — flow analysis narrows it:
Console.WriteLine(value.Length); // no warning
// Pattern matching also narrows:
if (value is { Length: > 0 })
Console.WriteLine(value.ToUpper()); // non-null
}
The null-forgiving operator !
! suppresses a nullable warning by asserting to the compiler "I know this is not
null." It has zero runtime effect — if you are wrong, you get a
NullReferenceException with no warning.
#nullable enable
// Legitimate use: field set in a method the analyser cannot track
private string _name = null!; // set in Initialize(), before any use
// Test setup:
private MyService _service = null!; // assigned in [SetUp] method
// After a manually verified check:
Debug.Assert(result != null);
Console.WriteLine(result!.Name); // analyser can't track Assert; ! suppresses warning
// Bad use — just hiding the problem:
string? couldBeNull = GetData();
Console.WriteLine(couldBeNull!.Length); // might blow up at runtime
Use ! sparingly — treat each occurrence as a TODO comment that deserves justification.
If you find yourself writing ! frequently, refactor the code so the analyser can
infer non-nullability.
Pattern matching and null
Pattern matching in C# provides clean, composable null handling that integrates with the compiler's flow analysis:
#nullable enable
object? Classify(object? value) => value switch
{
null => "nothing", // null arm
int n when n < 0 => $"negative: {n}",
int n => $"positive: {n}",
string { Length: 0 } => "empty string", // property pattern
string s => $"string: {s}",
_ => "something else",
};
// 'is' pattern with type check and null check in one:
if (value is string s) // only matches if value is non-null AND is string
Console.WriteLine(s.ToUpper());
// 'is not null' — preferred over '!= null' since C# 9:
if (value is not null)
Console.WriteLine(value.GetType());
// Property pattern with null check:
if (user is { Address.City: "London" }) // safely checks City even if Address could be null
Console.WriteLine("London user");
Boxing and null — the special case
When a null Nullable<T> is boxed, the result is a true null reference (not a
boxed wrapper around null). When a non-null Nullable<T> is boxed, the underlying
value T is boxed — not the Nullable<T> wrapper.
int? a = 42;
object boxedA = a; // boxes to boxed int (System.Int32) — NOT Nullable<int>
Console.WriteLine(boxedA.GetType().Name); // "Int32"
int? b = null;
object boxedB = b; // null reference — no heap allocation, no wrapper
Console.WriteLine(boxedB == null); // True
// Round-trip back:
int? recovered = (int?)boxedA; // 42
// int? wrong = (double?)boxedA; // InvalidCastException — was boxed as Int32
This design ensures null round-trips cleanly through object. A null int? assigned
to object gives you null back — not some "boxed null" pseudo-object.
Practical null-safety patterns
#nullable enable
// 1. Guard clause:
void Process(string? input)
{
ArgumentNullException.ThrowIfNull(input); // .NET 6+ one-liner
// input is non-null from here
}
// 2. Null object pattern — return a non-null default instead of null:
User GetUser(int id) => db.Users.Find(id) ?? User.Anonymous;
// 3. Optional pattern with records:
record struct Option<T>(T? Value, bool HasValue)
{
public static Option<T> Some(T value) => new(value, true);
public static Option<T> None => new(default, false);
}
// 4. Factory method instead of nullable constructor result:
// Instead of: new Parser(input)?.Parse()
// Use: Parser.TryCreate(input, out var parser) ? parser.Parse() : defaultResult
Recap
C# has two distinct null systems. Nullable value types (int? = Nullable<int>)
are a runtime CLR feature — a struct with HasValue and Value fields that can
represent null for any non-nullable value type. Nullable reference types (C# 8's
string? vs string) are a compile-time annotation feature with no runtime overhead —
they let the compiler perform null-state flow analysis and warn on potential
NullReferenceException before it happens. The ?? operator provides null coalescing,
?. provides safe navigation through object chains, and ??= is lazy assignment that
only fires on null. The null-forgiving operator ! suppresses analyser warnings when
you have a guarantee the tool cannot infer — use it sparingly. Pattern matching with
is not null, is { }, and switch expressions integrates null handling cleanly with
the compiler's flow analysis. Enabling <Nullable>enable</Nullable> in new projects
from day one catches most null bugs at compile time rather than at runtime.