Home C# / .NET C# Nullable Types Explained — How, Why, and When to Use Them

C# Nullable Types Explained — How, Why, and When to Use Them

In Plain English 🔥
Imagine a paper form with a field for 'Date of Birth'. Some forms are filled in completely — the field has a date. But what if someone deliberately left it blank? That blank isn't zero, and it isn't wrong — it genuinely means 'we don't know'. In C#, a regular int or DateTime can't be blank — they always hold a value. Nullable types are how you add that 'intentionally left blank' option to any value type.
⚡ Quick Answer
Imagine a paper form with a field for 'Date of Birth'. Some forms are filled in completely — the field has a date. But what if someone deliberately left it blank? That blank isn't zero, and it isn't wrong — it genuinely means 'we don't know'. In C#, a regular int or DateTime can't be blank — they always hold a value. Nullable types are how you add that 'intentionally left blank' option to any value type.

Every C# developer eventually hits the same wall: they're modelling real-world data — a database record, a web form, a sensor reading — and the data simply might not exist yet. A customer's loyalty points might be null because they've never made a purchase. A shipment's delivery date is null because it hasn't shipped yet. These aren't errors; they're valid business states. But if you reach for an int or a DateTime, C# won't let you express that state at all — those types must always contain a value.

Nullable types solve this by wrapping any value type in a container that adds one extra possibility: null. This is the difference between asking 'what is your score?' and 'do you even have a score yet?'. Without nullable types, developers resort to sentinel values — using -1 to mean 'no score', or DateTime.MinValue to mean 'no date' — and that produces bugs that are incredibly hard to track down because -1 looks like real data.

By the end of this article you'll understand exactly what int? means under the hood, how to safely read and write nullable values without crashing your app, how the null-coalescing and null-conditional operators make your code cleaner, and the common mistakes that send developers to Stack Overflow at 11pm. You'll also be ready to answer the nullable questions that pop up in virtually every C# interview.

What a Nullable Type Actually Is Under the Hood

When you write int? in C#, the compiler translates it to Nullable. That's not magic — it's a generic struct defined in the .NET base class library with exactly two properties: HasValue (a bool) and Value (the underlying int). That's the whole thing.

This matters for two reasons. First, it means a nullable type is still a value type — it lives on the stack, not the heap. There's no heap allocation, no garbage collector pressure. It's just a slightly bigger struct. Second, it means null for a nullable type doesn't mean 'a null reference' the way it does for a class. It means HasValue is false. The runtime never dereferences a pointer.

Why does that distinction matter? Because it explains the behaviour you'll see: you can assign null, you can compare with null, but if you try to read .Value when HasValue is false, you get an InvalidOperationException — not a NullReferenceException. That different exception type is a clue that something different is happening.

NullableInternals.cs · CSHARP
1234567891011121314151617181920212223242526272829303132333435363738394041
using System;

class NullableInternals
{
    static void Main()
    {
        // int? is just syntactic sugar for Nullable<int>
        // Both of these declarations are identical:
        int? playerScore = null;          // shorthand — what you'll write day-to-day
        Nullable<int> alsoPlayerScore = null; // longhand — what the compiler actually sees

        // HasValue tells you whether the nullable actually contains a number
        Console.WriteLine($"Has a score been set? {playerScore.HasValue}"); // False

        // Assign a real value
        playerScore = 4200;
        Console.WriteLine($"Has a score been set? {playerScore.HasValue}"); // True
        Console.WriteLine($"The score is: {playerScore.Value}");           // 4200

        // DANGER ZONE: Accessing .Value when HasValue is false
        // throws InvalidOperationException, NOT NullReferenceException
        int? unsetScore = null;
        try
        {
            int boom = unsetScore.Value; // this will throw
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine($"Caught: {ex.Message}");
            // Output: Nullable object must have a value.
        }

        // GetValueOrDefault() is the safe alternative — returns 0 if no value is set
        int safeScore = unsetScore.GetValueOrDefault(); // returns 0, no exception
        Console.WriteLine($"Safe fallback score: {safeScore}");            // 0

        // You can also provide a custom default
        int customDefault = unsetScore.GetValueOrDefault(defaultValue: -1);
        Console.WriteLine($"Custom fallback score: {customDefault}");      // -1
    }
}
▶ Output
Has a score been set? False
Has a score been set? True
The score is: 4200
Caught: Nullable object must have a value.
Safe fallback score: 0
Custom fallback score: -1
🔥
Interview Gold:If an interviewer asks 'what's the difference between a NullReferenceException and the exception you get from accessing .Value on an empty nullable?', the answer is InvalidOperationException. Knowing why — because nullable types are structs, not references — is what separates a good answer from a great one.

Real-World Nullable Patterns — The Operators That Do the Heavy Lifting

In production code you'll rarely write if (score.HasValue) by hand. C# gives you three operators that handle nullable logic concisely and safely. Learn these and your nullable code will be both shorter and more readable than the HasValue pattern.

The null-coalescing operator (??) returns the left side if it has a value, otherwise the right side. Think of it as 'use this, or fall back to that'. The null-coalescing assignment operator (??=) only assigns if the variable is currently null — perfect for lazy initialisation.

The null-conditional operator (?.) lets you call a method or property on something that might be null, and it short-circuits to null instead of throwing if it is null. This is primarily for reference types, but you'll frequently combine it with ?? when working with nullable value types retrieved from objects.

The as-a-team pattern is: use ?. to safely navigate to a nullable value, then ?? to provide a sensible default. Together they eliminate almost all defensive null-checking boilerplate.

RealWorldNullablePatterns.cs · CSHARP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
using System;

// Simulates a database row for a customer account
class CustomerAccount
{
    public string Name { get; set; }
    public int? LoyaltyPoints { get; set; }      // null means: never made a purchase
    public DateTime? LastPurchaseDate { get; set; } // null means: no purchase history
    public decimal? CreditLimit { get; set; }    // null means: not yet assessed
}

class RealWorldNullablePatterns
{
    // Returns the display string for loyalty points
    // Without nullable types you'd use -1 or 0 as a sentinel — both are misleading
    static string GetPointsDisplay(CustomerAccount account)
    {
        // ?? operator: "use LoyaltyPoints if it has a value, otherwise use 0"
        int pointsToShow = account.LoyaltyPoints ?? 0;
        return $"{pointsToShow} pts";
    }

    // Calculates days since last purchase, or returns null if no purchase exists
    static int? DaysSinceLastPurchase(CustomerAccount account)
    {
        // If LastPurchaseDate is null, this whole expression returns null — no crash
        // The cast to int? means the result can also be null
        return (int?)(DateTime.Today - account.LastPurchaseDate)?.TotalDays;
    }

    // Applies a credit limit, but only if one hasn't been set yet
    static void EnsureCreditLimit(CustomerAccount account, decimal defaultLimit)
    {
        // ??= operator: only assigns if CreditLimit is currently null
        account.CreditLimit ??= defaultLimit;
    }

    static void Main()
    {
        var newCustomer = new CustomerAccount
        {
            Name = "Priya Sharma",
            LoyaltyPoints = null,       // never purchased
            LastPurchaseDate = null,    // no purchase history
            CreditLimit = null          // not yet assessed
        };

        var regularCustomer = new CustomerAccount
        {
            Name = "James Okafor",
            LoyaltyPoints = 1850,
            LastPurchaseDate = DateTime.Today.AddDays(-12),
            CreditLimit = 500.00m
        };

        // ?? operator in action
        Console.WriteLine($"{newCustomer.Name}: {GetPointsDisplay(newCustomer)}");
        Console.WriteLine($"{regularCustomer.Name}: {GetPointsDisplay(regularCustomer)}");

        // null-conditional + ?? combo for safe navigation
        int? daysSinceNew = DaysSinceLastPurchase(newCustomer);
        int? daysSinceRegular = DaysSinceLastPurchase(regularCustomer);

        // ?? gives us a human-readable fallback when the value is null
        Console.WriteLine($"{newCustomer.Name} last purchased: {daysSinceNew?.ToString() ?? "Never"}");
        Console.WriteLine($"{regularCustomer.Name} last purchased: {daysSinceRegular} days ago");

        // ??= operator — only sets CreditLimit if it's currently null
        EnsureCreditLimit(newCustomer, defaultLimit: 250.00m);
        EnsureCreditLimit(regularCustomer, defaultLimit: 250.00m); // won't overwrite 500.00

        Console.WriteLine($"{newCustomer.Name} credit limit: {newCustomer.CreditLimit:C}");
        Console.WriteLine($"{regularCustomer.Name} credit limit: {regularCustomer.CreditLimit:C}");
    }
}
▶ Output
Priya Sharma: 0 pts
James Okafor: 1850 pts
Priya Sharma last purchased: Never
James Okafor last purchased: 12 days ago
Priya Sharma credit limit: £250.00
James Okafor credit limit: £500.00
⚠️
Pro Tip:Use ?? with a meaningful default that makes business sense, not just 0 or false. If 0 loyalty points and 'never set' loyalty points have different meanings in your domain, a nullable is exactly right — and ?? lets you present them differently to the user without corrupting the underlying data.

Nullables and Entity Framework — The Database Connection You Must Understand

The single most common place you'll encounter nullable types in professional C# is when mapping database columns. A SQL database column can be NOT NULL or NULL — and your C# model needs to reflect that truthfully. If it doesn't, you're lying to the compiler about your data, and bugs follow.

Entity Framework Core reads nullable properties on your model class and creates nullable columns in the database. Non-nullable properties create NOT NULL columns. This direct mapping means your C# type system is your database schema documentation — get the nullability right in C# and the database reflects reality.

There's a subtler point here too: when EF Core reads a nullable database column and the row contains NULL, it correctly populates your C# property as null. If you'd mapped that column to a non-nullable int, EF Core would throw an exception at runtime because it can't put NULL into an int. A lot of mysterious data-access bugs trace back to exactly this mismatch.

EntityFrameworkNullableMapping.cs · CSHARP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
using System;
using System.Collections.Generic;
using System.Linq;
// Simulated EF Core-style model — in a real project you'd reference
// Microsoft.EntityFrameworkCore and inherit from DbContext

// This model accurately reflects a real "orders" table
class Order
{
    public int OrderId { get; set; }             // NOT NULL in DB — always present
    public string CustomerEmail { get; set; }    // NOT NULL — required to place an order
    public DateTime OrderPlacedAt { get; set; }  // NOT NULL — timestamp is set on insert

    // These ARE nullable — they represent states that may not exist yet
    public DateTime? ShippedAt { get; set; }     // NULL until the item ships
    public DateTime? DeliveredAt { get; set; }   // NULL until delivery is confirmed
    public decimal? DiscountApplied { get; set; }// NULL if no discount was used
}

class EntityFrameworkNullableMapping
{
    // Simulates what EF Core would return from a database query
    static List<Order> GetSimulatedOrders()
    {
        return new List<Order>
        {
            new Order
            {
                OrderId = 101,
                CustomerEmail = "alice@example.com",
                OrderPlacedAt = DateTime.Today.AddDays(-5),
                ShippedAt = DateTime.Today.AddDays(-3),
                DeliveredAt = DateTime.Today.AddDays(-1),
                DiscountApplied = 15.00m
            },
            new Order
            {
                OrderId = 102,
                CustomerEmail = "bob@example.com",
                OrderPlacedAt = DateTime.Today.AddDays(-2),
                ShippedAt = DateTime.Today.AddDays(-1),
                DeliveredAt = null,          // not delivered yet
                DiscountApplied = null       // no discount used
            },
            new Order
            {
                OrderId = 103,
                CustomerEmail = "carol@example.com",
                OrderPlacedAt = DateTime.Today,
                ShippedAt = null,            // hasn't shipped yet
                DeliveredAt = null,
                DiscountApplied = 5.00m
            }
        };
    }

    static string GetOrderStatus(Order order)
    {
        // Pattern matching on nullable types — clean and expressive
        return order switch
        {
            // 'is not null' works naturally with nullable value types
            { DeliveredAt: not null } => $"Delivered on {order.DeliveredAt:dd MMM}",
            { ShippedAt: not null }   => "Shipped — awaiting delivery",
            _                         => "Processing"
        };
    }

    static void Main()
    {
        var orders = GetSimulatedOrders();

        foreach (var order in orders)
        {
            string status = GetOrderStatus(order);

            // ?? makes the discount display clean without if/else
            string discountInfo = order.DiscountApplied.HasValue
                ? $"Discount: {order.DiscountApplied:C}"
                : "No discount";

            Console.WriteLine($"Order #{order.OrderId} | {status} | {discountInfo}");
        }

        // LINQ works naturally with nullable types
        // Sum() on a nullable column requires the cast — this is a common real-world need
        decimal totalDiscounts = orders.Sum(o => o.DiscountApplied ?? 0m);
        Console.WriteLine($"\nTotal discounts given: {totalDiscounts:C}");

        // Find orders that are still pending (ShippedAt is null)
        int pendingCount = orders.Count(o => o.ShippedAt == null);
        Console.WriteLine($"Orders not yet shipped: {pendingCount}");
    }
}
▶ Output
Order #101 | Delivered on 29 Jun | Discount: £15.00
Order #102 | Shipped — awaiting delivery | No discount
Order #103 | Processing | Discount: £5.00

Total discounts given: £20.00
Orders not yet shipped: 1
⚠️
Watch Out:When you enable C# 8+ nullable reference types (the #nullable enable directive), the rules change for reference types like string too — and EF Core 6+ projects have this on by default. A non-nullable string property on your model will generate a NOT NULL column. If your database has existing NULL values in that column, EF Core will throw at runtime when it tries to map them. Always audit your column nullability when enabling this feature on an existing project.

Common Mistakes With Nullable Types and Exactly How to Fix Them

Nullable types have a small surface area, but there are specific mistakes that come up again and again — even from experienced developers. The two most damaging ones involve blindly accessing .Value and misunderstanding how null propagates through arithmetic.

A third, subtler mistake is using nullable types where you should be using the Null Object Pattern or a default value — nullable is the right tool when absence is meaningful, not when you just want to avoid initialising something.

Understanding these mistakes doesn't just save you from bugs — it makes your intent clearer to the next developer who reads your code. Code that correctly uses nullable types is self-documenting: it says 'this value might legitimately not exist, and we handle that case explicitly'.

NullableMistakesAndFixes.cs · CSHARP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
using System;

class NullableMistakesAndFixes
{
    static void Main()
    {
        // ─────────────────────────────────────────────────────────
        // MISTAKE 1: Casting a nullable to its base type without checking
        // ─────────────────────────────────────────────────────────

        int? temperatureReading = null; // sensor hasn't reported yet

        // BAD: This compiles fine but throws InvalidOperationException at runtime
        // int currentTemp = (int)temperatureReading;  // ← DON'T do this

        // FIX A: Use ?? to supply a safe fallback
        int currentTempFix1 = temperatureReading ?? -999; // -999 signals "no data"
        Console.WriteLine($"[Fix A] Temperature: {currentTempFix1}");

        // FIX B: Check HasValue before accessing Value
        if (temperatureReading.HasValue)
        {
            Console.WriteLine($"[Fix B] Temperature: {temperatureReading.Value}°C");
        }
        else
        {
            Console.WriteLine("[Fix B] Sensor has not reported yet.");
        }

        // ─────────────────────────────────────────────────────────
        // MISTAKE 2: Arithmetic with nullables produces null silently
        // Many developers expect this to throw — it doesn't
        // ─────────────────────────────────────────────────────────

        int? itemsInStock = null;    // stock count not yet loaded from database
        int reorderThreshold = 10;

        // BAD: This compiles and runs — but result is null, not an error!
        // If you then compare this to reorderThreshold, you'll get a wrong answer
        int? stockCheckResult = itemsInStock - reorderThreshold; // evaluates to null!
        Console.WriteLine($"\n[Mistake 2] Nullable arithmetic result: {stockCheckResult}");
        // Prints: [Mistake 2] Nullable arithmetic result:
        // (the null is just formatted as empty — silent and dangerous)

        // The comparison ALSO produces null (bool? not bool) — easy to misread
        bool? isBelowThreshold = itemsInStock < reorderThreshold;
        Console.WriteLine($"Is below threshold? {isBelowThreshold}"); // empty/null!

        // FIX: Decide what to do when the value isn't loaded yet, BEFORE the maths
        bool needsReorder;
        if (itemsInStock.HasValue)
        {
            needsReorder = itemsInStock.Value < reorderThreshold;
        }
        else
        {
            // Explicit business decision: treat unknown stock as needing a reorder
            needsReorder = true;
            Console.WriteLine("[Fix] Stock data unavailable — flagging for reorder review.");
        }
        Console.WriteLine($"[Fix] Needs reorder: {needsReorder}");

        // ─────────────────────────────────────────────────────────
        // MISTAKE 3: Using nullable when a default value is the right answer
        // ─────────────────────────────────────────────────────────

        // If 'no score' and '0 score' mean the SAME thing in your domain,
        // use int (defaulting to 0), not int?.
        // Only use int? when null carries a DIFFERENT meaning than any int value.

        // Example of CORRECTLY chosen nullable — null means "not yet graded"
        int? examScore = null;
        string gradeDisplay = examScore.HasValue
            ? $"Score: {examScore.Value}/100"
            : "Not yet graded";
        Console.WriteLine($"\n[Mistake 3 Fix] {gradeDisplay}");
    }
}
▶ Output
[Fix A] Temperature: -999
[Fix B] Sensor has not reported yet.

[Mistake 2] Nullable arithmetic result:
Is below threshold?
[Fix] Stock data unavailable — flagging for reorder review.
[Fix] Needs reorder: True

[Mistake 3 Fix] Not yet graded
⚠️
Watch Out:Nullable arithmetic is the most dangerous silent failure in C#. When either operand of +, -, *, / is null, the result is null — no exception, no warning at runtime. Always resolve nullability before performing calculations on values that drive business logic.
Aspectint (non-nullable)int? / Nullable
Can hold nullNo — compile errorYes — that's the whole point
Underlying typeValue type (struct)Value type (Nullable struct)
Memory size4 bytes5 bytes (4 + 1 bool for HasValue)
Exception on bad accessNone — always has a valueInvalidOperationException if .Value accessed when HasValue is false
Default value0null (HasValue = false)
Maps to SQL nullable columnNo — will throw on NULL dataYes — maps cleanly to NULL
Arithmetic with nullN/AResult is null — silent, not an exception
Works with pattern matchingYesYes — including 'is not null' and 'is null' checks
Use whenThe value must always existThe value legitimately might not exist yet

🎯 Key Takeaways

  • int? is syntactic sugar for Nullable — a struct with HasValue (bool) and Value (int). It's still a value type, so there's no heap allocation.
  • Always use ?? or check HasValue before touching the underlying value — accessing .Value on a null nullable throws InvalidOperationException, not NullReferenceException.
  • Nullable arithmetic is silent: null + anything = null. Never let a nullable flow untested into a calculation that drives real logic or a database write.
  • Only use nullable types when null has a genuine, distinct business meaning in your domain — if 'no value' and 'zero' mean the same thing, use the non-nullable type with a default value.

⚠ Common Mistakes to Avoid

  • Mistake 1: Directly casting a nullable to its value type without a null check — e.g. int score = (int)playerScore when playerScore is null — throws InvalidOperationException at runtime with the message 'Nullable object must have a value'. Fix: use playerScore ?? 0 for a safe fallback, or check playerScore.HasValue before accessing .Value.
  • Mistake 2: Expecting nullable arithmetic to throw when an operand is null — expressions like nullableA + nullableB silently evaluate to null instead of throwing. This produces wrong results downstream (often appearing as empty strings in the UI) with no error to trace. Fix: always check HasValue or use ?? to resolve nullability before performing calculations that influence logic or output.
  • Mistake 3: Using int? when a plain int with a sensible default is the right choice — overusing nullable types makes code harder to read and forces callers to handle null everywhere. Only reach for a nullable when null carries a distinct business meaning that cannot be expressed by any real value of the underlying type.

Interview Questions on This Topic

  • QWhat is Nullable in C#, and what are the two properties it exposes? How does it differ from a null reference on a class instance?
  • QWhat exception is thrown when you access .Value on a null nullable type, and why is it that specific exception rather than a NullReferenceException?
  • QGiven int? a = null and int? b = 5, what is the result of a + b, and what type is that result? What would a == b evaluate to? (Tests understanding of lifted operators and three-valued logic in nullable comparisons.)

Frequently Asked Questions

What is the difference between int and int? in C#?

int is a value type that must always contain an integer. int? (shorthand for Nullable) wraps that int in a struct that adds a HasValue flag, allowing it to also represent the absence of a value (null). Use int? when null has a meaningful, distinct state in your domain — like a database column that might be empty.

Can nullable types be used with all C# types?

Nullable works with value types only — structs and primitives like int, double, DateTime, bool, and enums. Reference types (classes, strings, arrays) can already hold null natively, so Nullable is invalid and won't compile. In C# 8+, the nullable reference types feature (string?) adds compiler warnings for reference types, but that's a separate, annotation-based system — not the same as Nullable.

Why does nullable arithmetic return null instead of throwing an exception?

C# implements 'lifted operators' for nullable types. When you apply an arithmetic operator like + or * to a nullable, and either operand is null, the result is null rather than an exception. This mirrors SQL's NULL arithmetic semantics and means unknown input produces unknown output. It's intentional but catches developers off guard because there's no runtime signal — always resolve null before arithmetic that matters.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousMocking with Moq in C#Next →Tuples in C#
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged