Nullable Types & Null Operators (??, ?., ??=)

Reviewed & published by Brayan K

"Missing data" is everywhere in real software — a user with no middle name, a setting that was never configured, a field left blank. In this lesson you'll learn how C# represents the absence of a value with null, how to let value types hold it, and the trio of operators (??, ?., ??=) that let you handle null safely without a pile of if checks.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a paper form with a box labelled "Middle name". A box with "James" written in it has a value. A box left blank isn't the same as a box with the word "blank" in it — it's empty. That emptiness is null. The danger is asking "how many letters are in the middle name?" when the box is blank — there's nothing to count, and the program crashes (NullReferenceException). The null operators are your way of saying "if the box is empty, do this instead" without panicking.

1. What Is null, and Nullable Value Types

Reference types (like string) have always been able to be null — they're a pointer to an object, and that pointer can point at nothing. Value types like int normally can't, because they store the number directly. Add a ? after the type to make a nullable value type that can hold null, and you gain two helpers: .HasValue and .Value.

using System;

class Program
{
    static void Main()
    {
        // null means "no value here at all" — an empty box with no object inside.
        // Reference types (like string) CAN already be null:
        string? middleName = null;        // the ? says "this may be null"
        Console.WriteLine(middleName == null);   // True

        // Value types (int, double, bool, DateTime...) normally CANNOT be null.
        // int age = null;  // ❌ compile error: Cannot convert null to 'int'

        // Add ? to a value type to make it NULLABLE — it can now hold null:
        int? score = null;                // a nullable int, currently empty
        Console.WriteLine(score.HasValue); // False  (no value stored)

        score = 90;                       // now give it a real value
        Console.WriteLine(score.HasValue); // True
        Console.WriteLine(score.Value);    // 90   (.Value reads the inner number)
    }
}

// ✅ Expected output:
//    True
//    False
//    True
//    90

2. The Null-Coalescing Operator ??

Read a ?? b as "a, unless a is null — then b". It's the single most common way to supply a default value, and it works for both nullable reference and nullable value types. You can even chain several together to pick the first thing that isn't null.

using System;

class Program
{
    static void Main()
    {
        // ?? is the NULL-COALESCING operator: "use the left value,
        // but if it's null, use the right value instead."
        string? input = null;
        string name = input ?? "Guest";
        Console.WriteLine(name);              // Guest  (input was null)

        string? input2 = "Maria";
        string name2 = input2 ?? "Guest";
        Console.WriteLine(name2);             // Maria  (input2 wasn't null)

        // It works for nullable value types too:
        int? maybeCount = null;
        int count = maybeCount ?? 0;          // default to 0 when null
        Console.WriteLine($"Count: {count}"); // Count: 0

        // You can chain it — the first non-null value wins:
        string? a = null, b = null, c = "found";
        Console.WriteLine(a ?? b ?? c ?? "none"); // found
    }
}

// ✅ Expected output:
//    Guest
//    Maria
//    Count: 0
//    found

Your turn — fill in the two blanks so a missing name and age fall back to sensible defaults.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".

        string? displayName = null;
        int? userAge = null;

        // 1) If displayName is null, fall back to "Anonymous"
        string finalName = ___;        // 👉 displayName ?? "Anonymous"

        // 2) If userAge is null, fall back to 18
        int finalAge = ___;            // 👉 userAge ?? 18

        Console.WriteLine($"{finalName} is {finalAge}");

        // ✅ Expected output:  Anonymous is 18
    }
}

3. The Null-Conditional Operator ?.

When you access a member like text.Length and text is null, the program throws a NullReferenceException — the most common crash in all of .NET. The ?. operator says "only do this if the thing on the left isn't null; otherwise the whole expression is just null". Pair it with ?? for the classic "safe access with a default" one-liner.

using System;

class Program
{
    static void Main()
    {
        // ?. is the NULL-CONDITIONAL operator (the "Elvis" / safe-navigation
        // operator). It calls a member ONLY if the object isn't null.
        string? text = null;

        // Without ?. this would throw NullReferenceException:
        // int len = text.Length;        // 💥 crash if text is null

        // With ?. the whole expression just returns null instead of crashing:
        int? len = text?.Length;
        Console.WriteLine(len.HasValue); // False  (text was null, so no Length)

        // Combine ?. with ?? for a clean "safe-with-default" pattern:
        int safeLen = text?.Length ?? 0;
        Console.WriteLine($"Length: {safeLen}"); // Length: 0

        string? real = "Hello";
        Console.WriteLine($"Length: {real?.Length ?? 0}"); // Length: 5

        // ?. also works for indexing collections that might be null:
        int[]? numbers = null;
        Console.WriteLine(numbers?[0] ?? -1); // -1  (no crash)
    }
}

// ✅ Expected output:
//    False
//    Length: 0
//    Length: 5
//    -1

4. The Null-Coalescing Assignment ??=

x ??= y is shorthand for "if x is null, set it to y". It's perfect for lazy initialisation — set something up the first time it's needed, and leave it alone after that. It reads cleaner than the equivalent if (x == null) x = y;.

using System;

class Program
{
    static void Main()
    {
        // ??= is the NULL-COALESCING ASSIGNMENT operator.
        // "Assign the right value ONLY if the left is currently null."
        string? config = null;

        config ??= "default-settings";   // config was null -> it becomes this
        Console.WriteLine(config);        // default-settings

        config ??= "ignored-value";       // config already has a value now
        Console.WriteLine(config);        // default-settings  (unchanged)

        // A very common real use: lazy initialisation of a list.
        System.Collections.Generic.List<string>? cache = null;
        cache ??= new System.Collections.Generic.List<string>();
        cache.Add("first item");
        Console.WriteLine($"Items: {cache.Count}"); // Items: 1
    }
}

// ✅ Expected output:
//    default-settings
//    default-settings
//    Items: 1

🧩 Reorder Challenge

These lines build a safe display name, but they're scrambled. Put them in the correct order so the code compiles and prints Hello, Guest! when raw is null.

A.     Console.WriteLine($"Hello, {name}!");
B. string? raw = null;
C.     string name = raw ?? "Guest";
D. static void Main()
E. {
F. }
D. static void Main()
E. {
B.     string? raw = null;
C.     string name = raw ?? "Guest";
A.     Console.WriteLine($"Hello, {name}!");
F. }

// Output:  Hello, Guest!

The declaration of raw must come before it's used in ??, and the method braces wrap everything.

🧠 Quick Recall

1. What does this print?

int? x = null;
Console.WriteLine(x ?? 99);

99 — x is null, so ?? falls back to the right-hand value.

2. What does this print?

string? s = "Hi";
Console.WriteLine(s?.Length ?? -1);

2 — s isn't null, so s?.Length is 2 and the ?? default is never used.

3. What does this print?

string? c = "set";
c ??= "other";
Console.WriteLine(c);

set — c already has a value, so ??= leaves it untouched.

Putting It Together: an Optional-Field Profile

Real records are full of optional fields. Here all three null operators work together to render a profile safely, no if statements in sight.

using System;

class Program
{
    // A user profile where several fields are optional (may be null).
    record UserProfile(string Name, string? Nickname, int? Age, string? Bio);

    static void Main()
    {
        var user = new UserProfile("Jordan", null, 28, null);

        // Use the operators together to render a safe, friendly profile.
        string display = user.Nickname ?? user.Name;          // prefer nickname
        string age     = user.Age?.ToString() ?? "unknown";   // safe + default
        int bioLength  = user.Bio?.Length ?? 0;               // 0 if no bio

        Console.WriteLine($"Display name: {display}");  // Jordan
        Console.WriteLine($"Age: {age}");               // 28
        Console.WriteLine($"Bio length: {bioLength}");  // 0

        // Without these operators every optional field needs an if-check —
        // three lines of code shrink the noise dramatically.
    }
}

// ✅ Expected output:
//    Display name: Jordan
//    Age: 28
//    Bio length: 0

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

OperatorMeansExample
T?Nullable typeint? n = null;
??Default if nulln ?? 0
?.Safe member accesss?.Length
??=Assign if nullx ??= 5;
.HasValueIs it set?n.HasValue
.ValueRead inner valuen.Value

Mini-Challenge: Safe Greeting

Use all three operators together. Read the brief in the comments, build it, and check your output against the example.

using System;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: Safe greeting
        // Given these (possibly null) values:
        string? firstName = null;
        string? lastName  = "Smith";
        int?    visits    = null;

        // 1. Build "fullName" preferring firstName, then lastName, then "Friend".
        // 2. Build "visitCount" that is visits, or 0 when null.
        // 3. Print:  "Welcome <fullName>! Visit #<visitCount + 1>"
        //
        // ✅ Example output:
        //    Welcome Smith! Visit #1

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What does the ?? null-coalescing operator do in 'a ?? b'?

  • Returns a, unless a is null — then b
  • Always returns b
  • Throws if a is null
  • Returns true if a is null

Answer: Returns a, unless a is null — then b. ?? supplies a default: it yields a, but falls back to b when a is null.

What is 'int?' equivalent to?

  • A string
  • Nullable<int>
  • An int array
  • A non-nullable int

Answer: Nullable<int>. int? is shorthand for the struct Nullable<int>, adding .HasValue and .Value and allowing null.

What does the ?. null-conditional operator do when the left side is null?

  • Throws NullReferenceException
  • Returns the whole expression as null
  • Returns 0
  • Continues anyway

Answer: Returns the whole expression as null. ?. short-circuits: if the object is null, the expression becomes null instead of throwing.

What does 'x ??= y' do?

  • Always sets x to y
  • Sets x to y only if x is currently null
  • Compares x and y
  • Reads x with a default

Answer: Sets x to y only if x is currently null. ??= is null-coalescing assignment: it assigns y to x only when x is null.

Why can value types like int not normally hold null?

  • They store the number directly, not a pointer to an object
  • They are too large
  • The compiler forbids it for performance
  • They are always nullable

Answer: They store the number directly, not a pointer to an object. Value types store the value directly; add ? (making int?) to let them hold null.

What happens if you call .Value on a nullable that is currently null?

  • Returns 0
  • Returns null
  • Throws InvalidOperationException
  • Returns false

Answer: Throws InvalidOperationException. Calling .Value on a null nullable throws InvalidOperationException; check .HasValue or use ?? instead.

In 'a ?? b ?? c', which value is produced?

  • The last value c always
  • The first non-null value, left to right
  • All three concatenated
  • null

Answer: The first non-null value, left to right. Chained ?? returns the first non-null operand from left to right.

What does '.HasValue' return for 'int? score = null;'?

  • True
  • False
  • 0
  • It throws

Answer: False. A nullable holding null has HasValue == false.

What is the cleanest 'safe access with a default' one-liner for a possibly-null string s?

  • s.Length
  • s?.Length ?? 0
  • s == null ? throw : s.Length
  • s!.Length

Answer: s?.Length ?? 0. Combine ?. with ?? — s?.Length ?? 0 never throws and gives 0 when s is null.

When should you use ?? versus ??=?

  • ?? reads a value with a fallback; ??= assigns a fallback into the variable
  • They are identical
  • ??= reads; ?? assigns
  • Both only work on int

Answer: ?? reads a value with a fallback; ??= assigns a fallback into the variable. Use ?? to read a value with a fallback; use ??= to assign a fallback into the variable only if it is null.

Continue this course

Frequently asked questions

What's the difference between string and string??

In a project with nullable reference types enabled, 'string' means the compiler expects it to never be null and warns you if it might be, while 'string?' explicitly says 'this can be null'. The '?' is a hint to both you and the compiler about your intent.

Is int? the same as Nullable<int>?

Yes — exactly the same. 'int?' is just convenient shorthand for the struct Nullable<int>. It adds .HasValue and .Value members and lets the variable hold null.

When should I use ?? vs ??=?

Use ?? when you want to READ a value with a fallback (string n = input ?? "Guest"). Use ??= when you want to ASSIGN a fallback into the variable itself only if it's currently null (config ??= LoadDefaults()).

Does ?. stop the whole chain on null?

Yes. In a?.b?.c, as soon as any link is null the entire expression short-circuits to null instead of throwing a NullReferenceException. It's the cleanest way to walk a chain of objects that might be missing.

Why did .Value throw an exception?

Calling .Value on a nullable that is currently null throws InvalidOperationException. Always check .HasValue first, or use ?? to provide a default rather than reaching for .Value directly.

Related lessons