Day 19: Nullable Types and Null Safety - Stop Your Code From Crashing on Nothing
arrow_back All Posts
April 06, 2026 10 min read .NET/C#

Day 19: Nullable Types and Null Safety - Stop Your Code From Crashing on Nothing

Yesterday you learned how to make your code wait patiently with async/await — no more freezing the screen while the internet takes its sweet time. Today we're confronting something far scarier than slow networks. We're talking about null — the silent assassin lurking inside your variables, waiting for the exact wrong moment to crash your entire application.

Think of it like package delivery. You order a book online. Sometimes the box arrives with the book inside. Sometimes the box arrives empty. And sometimes — here's the fun part — there's no box at all. Just a delivery driver staring at you, shrugging. Your code has to handle all three scenarios, and the absolute worst thing you can do is rip open the box without checking if it exists first.

1. What Is Null?

null means "nothing." Not zero. Not an empty string. Not false. Just... nothing. The absence of an object. An empty chair with a name tag that says "Bob" — Bob isn't late, Bob doesn't exist.

string name = "Farhad";  // name points to a string object in memory
string nobody = null;     // nobody points to... nothing. The void. The abyss.

Console.WriteLine(name.Length);   // 6 — works fine
Console.WriteLine(nobody.Length); // 💥 NullReferenceException!

That second line crashes your program. Not a polite error message. Not a warning. A full-on explosion. You tried to ask "nothing" how long it is, and .NET responded by flipping the table.

Value types like int, bool, and double can't be null by default. They always have a value — int defaults to 0, bool defaults to false. They're like a chair that always has someone sitting in it, even if that someone is a mannequin. Reference types like string, arrays, and your custom classes? They default to null. Empty chair, no mannequin, just air.

2. The Billion-Dollar Mistake

In 1965, a computer scientist named Tony Hoare invented the concept of null references. Decades later, he called it his "billion-dollar mistake" — and he wasn't being dramatic. NullReferenceException is the single most common crash in .NET applications. It's the #1 cause of production incidents, support tickets, and 3 AM wake-up calls.

Every language that inherited null references — C#, Java, JavaScript, C++ — has suffered the same plague. Your code compiles fine. Your tests pass. Then someone passes null where you expected an object, and everything burns.

// This compiles perfectly. It's also a ticking time bomb.
static int GetNameLength(string name)
{
    return name.Length; // What if name is null? 💥
}

Console.WriteLine(GetNameLength("Farhad")); // 6
Console.WriteLine(GetNameLength(null));     // NullReferenceException!

The compiler saw nothing wrong. The code is syntactically valid. But semantically, it's a trap — and C# has spent the last several versions building tools to help you avoid it.

3. Nullable Value Types — Teaching Integers to Be Empty

Normally, an int always holds a number. You can't set int x = null; — the compiler won't let you. But sometimes you genuinely need to say "this number doesn't exist." A user's age on a form they haven't filled out yet. A database column that allows NULL. A score for a game that hasn't been played.

Enter nullable value types. Slap a ? on any value type, and now it can be null:

int? age = null;          // "We don't know the age yet"
double? temperature = null; // "Sensor hasn't reported"
bool? accepted = null;     // "User hasn't responded"

if (age.HasValue)
{
    Console.WriteLine($"Age is {age.Value}");
}
else
{
    Console.WriteLine("Age is unknown");
}

// Or use GetValueOrDefault
int displayAge = age.GetValueOrDefault(0);
Console.WriteLine($"Display age: {displayAge}"); // 0

int? is actually syntactic sugar for Nullable<int> — a struct that wraps the value type and adds a HasValue property. It's a box around your value that can be empty. Remember the delivery analogy? int? is the box that might or might not contain a number.

// These are the same thing
int? shortWay = 42;
Nullable<int> longWay = 42;

// Null means "no value" — not zero!
int? score = null;
int? zero = 0;
Console.WriteLine(score.HasValue); // False
Console.WriteLine(zero.HasValue);  // True — 0 is a value!

4. The Null-Conditional Operator — Safe Navigation

Before C# 6, checking for null was a tedious if pyramid:

// The old, painful way
string city = null;
if (customer != null)
{
    if (customer.Address != null)
    {
        city = customer.Address.City;
    }
}

Three levels deep, and we're still not sure we got it right. The null-conditional operator (?.) collapses all of this into one line:

string? city = customer?.Address?.City;

That's it. If customer is null, the whole expression returns null — no exception. If customer exists but Address is null, same thing. It short-circuits at the first null and gives up gracefully, like a delivery driver who sees the house doesn't exist and just goes home instead of trying to kick down the door.

It works with methods too:

// Only calls ToUpper() if name isn't null
string? upper = name?.ToUpper();

// Works with array indexing
int? firstScore = scores?[0];

// Chain as deep as you want
int? nameLength = order?.Customer?.Name?.Length;

And there's the null-conditional element access (?[]) for arrays and indexers. If the array itself is null, you get null back instead of a crash.

5. The Null-Coalescing Operator — Defaults Without the Fuss

So ?. gives you null when something's missing. But often you don't want null — you want a fallback value. That's what ?? does:

string name = inputName ?? "Anonymous";
// If inputName is null, use "Anonymous"

int pageSize = userPreference ?? 25;
// If userPreference (an int?) is null, use 25

string display = user?.Profile?.DisplayName ?? user?.Email ?? "Unknown User";
// Try display name, then email, then give up

Read ?? as "or if that's null, use this instead." You can chain them. The first non-null value wins.

There's also ??=, the null-coalescing assignment operator — it only assigns if the variable is currently null:

List<string>? names = null;

// Only creates the list if names is null
names ??= [];

names.Add("Alice"); // Safe — names is guaranteed non-null here

// Equivalent to:
// if (names == null) { names = []; }

This is perfect for lazy initialization. Don't create something until you actually need it — and if it already exists, leave it alone.

6. Nullable Reference Types — The Compiler Has Your Back

Here's where C# got serious about null safety. Starting in C# 8 (.NET Core 3.0+), you can enable nullable reference types — a feature that makes the compiler warn you when you're doing something risky with null.

In your .csproj file (or at the top of a file), enable it:

<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>

Now the rules change. A plain string means "this should never be null." If you want to allow null, you write string?:

// With nullable reference types enabled:
string definitelyExists = "hello";   // Non-nullable — the compiler trusts this is never null
string? mightBeNull = null;          // Nullable — the compiler knows this could be null

Console.WriteLine(definitelyExists.Length); // Fine — no warning
Console.WriteLine(mightBeNull.Length);      // ⚠️ Warning CS8602: possible null reference

The compiler won't stop you from compiling — these are warnings, not errors. But they're really good warnings. They catch the exact scenarios that cause NullReferenceException at runtime.

static string Greet(string? name)
{
    // Compiler warns: 'name' might be null
    // return $"Hello, {name.ToUpper()}!"; // ⚠️ Warning!

    // Fix option 1: null check
    if (name is null)
        return "Hello, stranger!";
    return $"Hello, {name.ToUpper()}!"; // No warning — compiler knows name isn't null here

    // Fix option 2: null-coalescing
    // return $"Hello, {(name ?? "stranger").ToUpper()}!";
}

The compiler does flow analysis — it tracks whether a variable has been checked for null and adjusts its warnings accordingly. After an if (name is null) return; guard, the compiler knows that name is non-null for the rest of the method. Smart.

7. Pattern Matching With Null

C# gives you elegant ways to check for null using pattern matching — and they read almost like English:

string? message = GetMessage();

// is null / is not null (preferred over == null)
if (message is null)
{
    Console.WriteLine("No message");
}

if (message is not null)
{
    Console.WriteLine($"Message: {message}");
}

Why prefer is null over == null? Because == can be overloaded. A class could redefine what == means and accidentally break your null check. is null always checks for actual null — no operator overloading can interfere.

Pattern matching shines in switch expressions:

string Describe(object? value) => value switch
{
    null => "nothing at all",
    string s => $"a string: \"{s}\"",
    int n when n > 0 => $"a positive number: {n}",
    int n => $"a number: {n}",
    _ => $"something of type {value.GetType().Name}"
};

Console.WriteLine(Describe(null));      // nothing at all
Console.WriteLine(Describe("hello"));   // a string: "hello"
Console.WriteLine(Describe(42));        // a positive number: 42
Console.WriteLine(Describe(-7));        // a number: -7
Console.WriteLine(Describe(3.14));      // something of type Double

Putting null as the first case in a switch is a common pattern — handle the "nothing" case early, then deal with the real values.

8. Best Practices — Fighting Null in the Real World

You now have all the weapons. Here's how to use them wisely:

Return empty collections, not null. If a method returns a list of results and finds nothing, return an empty list. Callers can iterate over an empty list without crashing. They can't iterate over null.

// Bad — forces every caller to check for null
static List<string>? GetTagsBad(string post)
{
    if (post == "draft") return null; // 💣
    return ["csharp", "dotnet"];
}

// Good — callers can always iterate safely
static List<string> GetTagsGood(string post)
{
    if (post == "draft") return []; // Empty but safe
    return ["csharp", "dotnet"];
}

Use guard clauses. Check for null at the top of your methods and fail fast:

static void SendEmail(string to, string body)
{
    ArgumentNullException.ThrowIfNull(to);
    ArgumentNullException.ThrowIfNull(body);

    // Rest of method — no null worries from here on
    Console.WriteLine($"Sending to {to}: {body}");
}

ArgumentNullException.ThrowIfNull was added in .NET 6 and is the cleanest way to guard your inputs. One line per parameter, no if statements cluttering up the top of your method.

The Null Object Pattern. Instead of returning null, return an object that does nothing — a "null object" that satisfies the interface but has no behavior:

interface ILogger
{
    void Log(string message);
}

class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
}

class NullLogger : ILogger
{
    public void Log(string message) { } // Does nothing — silently
}

// Now you never need to check for null
ILogger logger = shouldLog ? new ConsoleLogger() : new NullLogger();
logger.Log("This works regardless"); // No null check needed

No if (logger != null) scattered everywhere. The NullLogger is a real object — it just doesn't do anything. Elegant.

9. Your Homework: Make This Crash-Proof

Here's a method that crashes if you look at it wrong. Refactor it to be completely null-safe using the techniques from today:

// BEFORE: This crashes on null input
static void PrintUserInfo(string? name, string? email, int? age)
{
    // Your job: make this method handle ALL null combinations
    // without crashing, using ?., ??, ??=, is null, and pattern matching.
    //
    // Requirements:
    // - If name is null, use "Anonymous"
    // - If email is null, use "no-email@example.com"
    // - If age is null, print "age unknown" instead of a number
    // - Print a formatted summary like:
    //   "User: FARHAD (farhad@example.com) - Age: 30"
    //   (name should be uppercase)
    //
    // Bonus: Add a method that takes a List<string>? of hobbies
    // and prints them. If the list is null, use an empty list.
    // If any hobby in the list is null, skip it.

    Console.WriteLine($"User: {name.ToUpper()} ({email}) - Age: {age}");
}

// These should ALL work without crashing:
PrintUserInfo("Farhad", "farhad@example.com", 30);
PrintUserInfo(null, null, null);
PrintUserInfo("Alice", null, 25);
PrintUserInfo(null, "bob@test.com", null);

Summary of Day 19

  • null means "no object" — not zero, not empty, just nothing. Accessing members on null throws NullReferenceException.
  • Nullable value types (int?, bool?) let value types hold null by wrapping them in Nullable<T>.
  • ?. (null-conditional) safely navigates chains of member access — short-circuits to null at the first miss.
  • ?? (null-coalescing) provides a fallback value when something is null. ??= assigns only if currently null.
  • Nullable reference types (C# 8+) make the compiler warn you about potential null dereferences — enable with <Nullable>enable</Nullable>.
  • is null and is not null are the safest way to check for null — immune to operator overloading.
  • Return empty collections instead of null, use guard clauses with ArgumentNullException.ThrowIfNull, and consider the Null Object Pattern for interface-heavy code.

Tomorrow: we'll talk about Records and Pattern Matching — building immutable data types that compare by value and matching them with switch expressions like a pro. If classes are houses, records are prefab modular homes — instant, identical, and surprisingly practical. 🚀

See you on Day 20!

Share
FM

Farhad Mammadov

.NET Engineer & Cloud Architect · Bayern, Germany. Writing about scalable backend systems, AWS, and SRE.