Mid-level 8 min · March 06, 2026

C# Nullable Arithmetic — Silent £0.00 from Null BasePrice

A null basePrice in nullable arithmetic returns null, then ?? 0m silently produces £0.00.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Nullable wraps any value type with a HasValue flag
  • Use ?? to provide a default when null, not if statements
  • Nullable arithmetic silently returns null — no exception thrown
  • 5 bytes vs 4 bytes for int? vs int (one extra byte for bool)
  • Accessing .Value on null throws InvalidOperationException, not NullReferenceException
  • EF Core maps nullable C# properties directly to nullable SQL columns
✦ Definition~90s read
What is Nullable Types in C#?

C# nullable types solve the problem of representing 'no value' for value types like int, decimal, or DateTime — things that normally can't be null. Under the hood, a Nullable<T> is a struct wrapping the value with a HasValue boolean flag. When you write decimal? price = null, the compiler generates a Nullable<decimal> where HasValue is false.

Imagine a paper form with a field for 'Date of Birth'.

The real-world gotcha: arithmetic on nullables propagates null. null + 5 is null, and null * 1.2m is null. That's why BasePrice being null silently produces £0.00 in your UI — you're likely formatting a null result as 0.ToString("C") or hitting the default of a null-coalescing operator you didn't intend.

In the ecosystem, nullable types are essential for database mapping (Entity Framework maps SQL NULL to null for nullable columns), but they're not a substitute for Nullable<T> in hot paths where boxing overhead matters — use Nullable<T> directly or Maybe<T> monads if you need functional purity. The two declaration syntaxes — Nullable<decimal> vs decimal? — are identical at the IL level; decimal? is just syntactic sugar.

Always prefer the ? syntax for readability, but know that Nullable<T> is what actually lives in memory.

Plain-English First

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<int>. 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.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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.
Production Insight
A late-night outage traced back to unsetScore.Value — devs assumed null would throw NullReferenceException and caught the wrong type.
Always catch InvalidOperationException when working with nullable .Value, or better: avoid .Value entirely.
Rule: treat nullable .Value as code smell — use ?? or pattern matching instead.
Key Takeaway
int? is a struct, not a reference.
HasValue=false means null — never dereference .Value without checking.
The exception you get is InvalidOperationException — not NullReferenceException.
C# Nullable Arithmetic Pitfalls THECODEFORGE.IO C# Nullable Arithmetic Pitfalls How null BasePrice silently yields £0.00 in calculations Nullable Value Type Underlying struct with HasValue flag Null Coalescing (??) Provides default when null Arithmetic with Null Null propagates; result is null Entity Framework Mapping Nullable columns map to Nullable Pattern Matching Check Use 'is null' or 'is not null' Safe Calculation Coalesce before arithmetic ⚠ Null + number = null, not zero Always coalesce (??) before arithmetic operations THECODEFORGE.IO
thecodeforge.io
C# Nullable Arithmetic Pitfalls
Nullable Types Csharp

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.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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.
Production Insight
A production bug where ?? 0 was used for a 'days since purchase' field — 0 meant 'today' but null meant 'never'. The UI displayed 0 for new customers, confusing the support team.
The fix: use a sentinel like -1 and check in display logic.
Rule: make ?. + ?? the default pattern — it reduces boilerplate and prevents null crashes.
Key Takeaway
?? and ??= are the primary tools for clean nullable handling.
?. + ?? combo eliminates most manual null checks.
Avoid using ?? with a default that masks meaningful null states.

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.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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.
Production Insight
A migration added a non-nullable string column to an existing table with NULL values — EF Core crashed on read with InvalidOperationException.
The fix: either add a default constraint to the database or make the property nullable in C#.
Rule: always match C# nullability to the database column — the type system is your schema contract.
Key Takeaway
EF Core maps nullable C# properties to nullable SQL columns and vice versa.
A mismatch causes runtime exceptions — not compilation errors.
Audit nullability when enabling nullable reference types on existing projects.

Nullable Types and Pattern Matching — Cleaner State Handling

C# 7+ introduced pattern matching that works beautifully with nullable types. You can check for null directly with the 'is null' and 'is not null' patterns, and you can even switch on nullable values. This leads to code that reads like the business logic itself — not like defensive programming.

Before pattern matching, you'd write if (score.HasValue) { ... } else { ... }. Now you can write if (score is not null) { ... }. It's a small change, but it makes your intent instantly clear: you're checking whether a value exists, not whether a property is true.

Switch expressions take this further. You can match on nullable properties of an object directly, combining property patterns with null checks. This is especially powerful in domain logic like order processing, where the state of an order depends on which nullable timestamps are set.

PatternMatchingWithNullables.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;

class PatternMatchingWithNullables
{
    enum OrderStatus { Pending, Shipped, Delivered, Unknown }

    // Returns order status using switch expression with property patterns
    static OrderStatus GetOrderStatus(Order order) => order switch
    {
        { DeliveredAt: not null } => OrderStatus.Delivered,
        { ShippedAt: not null }   => OrderStatus.Shipped,
        _                         => OrderStatus.Pending
    };

    static string DescribeDiscount(decimal? discount) => discount switch
    {
        null => "No discount applied",
        > 100 => "Generous discount!",
        > 0  => $"Discount of {discount:C}",
        0    => "Coupon used but $0 discount",
        _    => $"Unexpected discount value: {discount}"
    };

    static void Main()
    {
        var order1 = new Order { ShippedAt = null, DeliveredAt = null };
        var order2 = new Order { ShippedAt = DateTime.Now, DeliveredAt = null };
        var order3 = new Order { ShippedAt = DateTime.Now, DeliveredAt = DateTime.Now };

        Console.WriteLine($"Order status: {GetOrderStatus(order1)} (expected: Pending)");
        Console.WriteLine($"Order status: {GetOrderStatus(order2)} (expected: Shipped)");
        Console.WriteLine($"Order status: {GetOrderStatus(order3)} (expected: Delivered)");

        Console.WriteLine(DescribeDiscount(null));        // No discount applied
        Console.WriteLine(DescribeDiscount(50m));         // Discount of £50.00
        Console.WriteLine(DescribeDiscount(150m));        // Generous discount!
    }
}

// Simple Order class for demonstration
class Order
{
    public DateTime? ShippedAt { get; set; }
    public DateTime? DeliveredAt { get; set; }
}
Output
Order status: Pending (expected: Pending)
Order status: Shipped (expected: Shipped)
Order status: Delivered (expected: Delivered)
No discount applied
Discount of £50.00
Generous discount!
Pro Tip:
Use 'is not null' in if conditions for nullable types — it reads closer to natural language than HasValue. For more complex logic, switch expressions with property patterns give you both readability and exhaustiveness checking.
Production Insight
A bug where a switch expression didn't cover the case where a nullable had a negative value — the '_' wildcard caught it, but the business logic expected a positive discount. Pattern matching with nullable types makes it easy to add explicit cases for invalid states.
Rule: treat nullable pattern matching as the default way to handle nullable state in new code.
Key Takeaway
Use 'is null' / 'is not null' over HasValue in if statements.
Switch expressions with property patterns handle nullable states elegantly.
Pattern matching eliminates the need for .Value access — safer and cleaner.

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.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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.
Production Insight
The silent null arithmetic bug is the most common nullable issue in production. A stock calculation that returned null instead of a number caused an automated reorder system to do nothing — stock ran out.
The fix was to enforce non-nullable math by resolving nulls with ?? before any calculation.
Rule: treat nullable arithmetic as a code smell — always resolve nulls first.
Key Takeaway
Never cast nullable to its base type without checking.
Nullable arithmetic returns null silently — always resolve before math.
Only use nullable when null has a distinct meaning from any value.

Nullable Syntax — The Two Ways to Declare and Why One Is Better

There are two syntaxes for nullable value types: Nullable<T> and the T? shorthand. They compile to identical IL. One of them you should never write in production. Nullable<int> is verbose, clutters code, and tells the next engineer you don't trust the type system. int? is the idiomatic C# way. Use it. The ? suffix signals intent: this variable can be null. No ceremony. For reference types in C# 8+, string? follows the same pattern — it turns on null-state analysis and the compiler will enforce null checks. The old Nullable<T> syntax survives for legacy interop and generic constraints. If you're writing a generic method or a constraint like where T : struct, you'll see Nullable<T> in signatures. That's fine. Everywhere else: use the ? operator. Readability wins.

NullableDeclarations.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — csharp tutorial

// What you should write in production
int? orderCount = null;
double? discountRate = 0.15;
bool? isVerified = true;

// What compiles to the same thing but makes you look like an intern
Nullable<int> legacyOrderCount = null;

// This compiles but is redundant noise
int? explicitNull = null;

// Reference type nullable — C# 8+ null-state analysis
string? customerName = null;

Console.WriteLine($"Orders: {orderCount?.ToString() ?? "none"}");
Console.WriteLine($"Rate: {discountRate}");
Console.WriteLine($"Verified: {isVerified}");
Output
Orders: none
Rate: 0.15
Verified: True
Production Trap:
Mixing Nullable<int> and int? in the same codebase confuses new hires and static analysis tools. Pick one. The team picks the ? operator. Always.
Key Takeaway
Declare nullable value types with the ? operator. Nullable<T> is for generic constraints only.

Accessing Nullable Values — Don’t Get Caught by the Default

You cannot read a nullable value directly. The compiler forces you to check for null. That's a feature, not a bug. There are three ways to access the value. GetValueOrDefault() is the most common — it returns the stored value or the default for that type (0 for int, false for bool, etc.). This is fine when a zero default makes sense. It's a landmine when it doesn't. If a null order count of 0 means 'no orders' and a real zero means 'all orders cancelled', you just lost a bug report. Use the null-conditional operator ?. to short-circuit safely. Or use Value if you've already checked HasValue. Never access .Value without a guard — that throws InvalidOperationException at runtime. Pattern matching with switch or is is cleaner: you match null and valid states explicitly. The GetValueOrDefault(T defaultValue) overload lets you supply your own fallback. Use that when zero is wrong.

AccessPatterns.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — csharp tutorial

int? itemsShipped = null;

// Dangerous — silently returns 0
int shippedCount = itemsShipped.GetValueOrDefault();
Console.WriteLine($"Shipped (default): {shippedCount}");

// Better — explicit fallback
int safeCount = itemsShipped.GetValueOrDefault(-1);
Console.WriteLine($"Shipped (safe): {safeCount}");

// Clean pattern matching (C# 7+)
string status = itemsShipped switch
{
    null => "Pending",
    > 0 => $"{itemsShipped} shipped",
    _ => "Zero shipped"
};
Console.WriteLine($"Status: {status}");

// Throws InvalidOperationException if null — use only after HasValue check
// int crash = itemsShipped.Value; // DON'T
Output
Shipped (default): 0
Shipped (safe): -1
Status: Pending
Production Trap:
GetValueOrDefault() with no argument returns 0. If 0 means 'processed' in your domain and null means 'not yet', you just reported a false positive. Always pass an explicit sentinel value or use pattern matching.
Key Takeaway
Use GetValueOrDefault(TDefault) or pattern matching to access nullable values. Never assume zero is safe.

The Null Coalescing Operator (??) — Your Shortcut to Ugly Boilerplate

The ?? operator unwraps a nullable into a non-nullable value, substituting a default if the source is null. It's syntactic sugar for a ternary: x != null ? x : defaultValue. But it's better because it's terse and forces you to specify the fallback at the call site. Do not confuse ?? with ?. (null-conditional). ?? returns a value; ?. lets you access members safely. Chaining them is common: customer?.Address?.City ?? "Unknown". That reads: navigate the object graph, return null if anything is null, then substitute. Production note: the right-hand side of ?? is lazily evaluated. That matters when the fallback is an expensive method call or a new allocation. If you write customer?.OrderTotal ?? CalculateDefaultOrder(), that method only runs when OrderTotal is null. Good for performance. Bad for side effects — don't hide a database call or a logging statement there. Keep it pure. The ??= operator (C# 8+) assigns the right side only if the left is null. Perfect for lazy initialization patterns.

CoalescingPatterns.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — csharp tutorial

int? cachedTotal = null;

// ?? — provide fallback
int displayTotal = cachedTotal ?? 0;
Console.WriteLine($"Display: {displayTotal}");

// ??= — lazy init (C# 8+)
cachedTotal ??= ComputeExpensiveTotal();
Console.WriteLine($"Cached: {cachedTotal}");

// Chaining with null-conditional
var customer = new { Name = "Alice", Address = (string?)null };
string city = customer.Address?.Split(',')[1]?.Trim() ?? "Unknown";
Console.WriteLine($"City: {city}");

int ComputeExpensiveTotal()
{
    Console.WriteLine("Computing...");
    return 42;
}
Output
Display: 0
Computing...
Cached: 42
City: Unknown
Senior Shortcut:
Use ??= for one-time initialization patterns (e.g., Lazy<T> replacement). It's thread-safe when the left-hand side is a field — the runtime handles the double-checked locking for you.
Key Takeaway
Use ?? to provide fallback values. Use ??= for lazy initialization. Keep the right-hand side side-effect-free.

Boxing and Unboxing Nullable Types — The Hidden Performance Cost

When you assign a nullable value type to a non-nullable reference (like object), the runtime performs boxing: it wraps the value in a heap object. For nullable types, this boxing behaves differently than you might expect. If the nullable has a value, the runtime boxes the underlying type — not the Nullable<T> wrapper. If the nullable is null, boxing produces a null reference directly — no boxed struct. This asymmetry matters because unboxing from a boxed nullable value recovers the underlying type, not a Nullable<T>. For example, boxing int? x = 5 gives an object that is a boxed int, unboxable only to int. Boxing int? y = null gives null. Mistake: trying to unbox a formerly-nullable boxed int? back to int? causes an InvalidCastException. You must unbox to the underlying type, then wrap if needed. Performance: avoid repeated boxing in hot paths — each allocation and GC pressure adds up. Prefer generics or direct value access when possible.

BoxingNullables.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — csharp tutorial

int? maybe = 42;
object boxed = maybe;    // boxes to int, not Nullable<int>
int unboxed = (int)boxed; // OK

int? nullMaybe = null;
object boxedNull = nullMaybe; // null reference

// Wrong: (int?)boxed throws InvalidCastException
// Right: cast to int, then assign to int?
int? correct = (int)boxed;

int? another = 100;
object boxed2 = another;
bool isBoxedInt = boxed2 is int; // true
Output
// (No output — code compiles and runs without exception)
Production Trap:
Never unbox a boxed nullable value directly back to the same nullable type. The runtime stores the underlying value, not the Nullable<T> wrapper. Always unbox to the base type first.
Key Takeaway
Boxing a nullable value boxes the underlying type, not the Nullable<T>. Unbox correctly: cast to the base type, then wrap in nullable if needed.

Practical Examples: Assignments by Difficulty Level

Applying nullable types correctly requires matching complexity to context. Beginner: use nullable for optional fields like MiddleName or nullable database columns. Example: string? middleName = null; if (middleName != null) { ... } matches the intent. Intermediate: use pattern matching with nullable enums or result types. Example: StatusCode? result = GetResult(); string message = result switch { 200 => "OK", 404 => "Not Found", null => "Unknown", _ => "Other" }; Advanced: combine nullable with generic constraints or value-task patterns. Example: async Task<T?> FetchAsync<T>(int id) where T : struct { ... } returns null on miss. Performance-critical: avoid repeated null checks on value types; use Nullable.GetValueOrDefault() with explicit default. In Entity Framework, null coalescing in queries (e.g., db.People.Select(p => p.MiddleName ?? "N/A")) translates to SQL COALESCE. Mistmatch difficulty: using null-forgiving operator (!) in simple validation code creates hidden null risks — reserve for interop or tests only.

NullableDifficulty.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — csharp tutorial

// Beginner
string? middleName = null;
if (middleName is not null)
    Console.WriteLine(middleName);

// Intermediate
int? score = GetScore();
string grade = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    null  => "Incomplete",
    _     => "Fail"
};

// Advanced
async Task<T?> FetchAsync<T>(int id) where T : struct
{
    return id == 0 ? null : (T)(object)id;
}
Output
// (No output — compiles and demonstrates pattern)
Production Trap:
Using null-forgiving operator (!) in business logic bypasses compiler null-state analysis. Reserve for test code or legacy interop — never for user-facing validation.
Key Takeaway
Match nullable usage to skill level: beginners use simple null checks, intermediates use pattern matching, advanced use generics with constraints. Never overcomplicate simple scenarios.
● Production incidentPOST-MORTEMseverity: high

The Silent Order Cancellation — Nullable Arithmetic in a Discount Engine

Symptom
Orders with a valid discount code showed £0.00 discount in the invoice. The discount percentage was correctly stored, but the final amount appeared as zero. Support logs showed no errors — just a mysterious empty column.
Assumption
The team assumed that if the discount percentage was not null, the arithmetic would produce a number. They checked the discount percentage — non-null — but missed that the product base price was retrieved from a different source that could return null.
Root cause
The discount calculation used: decimal? finalAmount = basePrice * (1 - discountPercent / 100). Both basePrice and discountPercent were nullable decimals. When basePrice was null (because a legacy product didn't have a price in the new system), the entire expression silently returned null. The code then assigned that null to a non-nullable decimal via ?? 0m, producing £0.00.
Fix
Moved the null resolution before the arithmetic: decimal actualBase = basePrice ?? 0m; decimal discountFactor = 1 - (discountPercent ?? 0m) / 100; finalAmount = actualBase * discountFactor;. Also added a validation step to log warnings when basePrice was null.
Key lesson
  • Never let nullable values flow into arithmetic without resolving nulls first.
  • Use ?? to provide safe defaults before any calculation involving nullable operands.
  • Add explicit logging or validation when a nullable is null but the business expects it to have a value.
  • Test discount pipelines with deliberately missing data — sentinel values hide null arithmetic.
Production debug guideSymptom → Quick Action → Root Cause → Fix4 entries
Symptom · 01
InvalidOperationException: Nullable object must have a value.
Fix
Check the stack trace for .Value access. Replace with GetValueOrDefault() or ?? operator. Add nullable logging before the crash.
Symptom · 02
UI shows empty string or 0 where you expect a number
Fix
Inspect the variable in the debugger. Look for nullable types that evaluated to null in mathematical expressions. Check HasValue.
Symptom · 03
EF Core query returns rows but some model properties are unexpectedly 0 or default
Fix
Examine the database column nullability. If column allows NULL but C# property is non-nullable, EF will throw on materialization — not silently. Check for missing nullable annotations in model.
Symptom · 04
Comparison produces unexpected results (e.g. null < 5 is false)
Fix
Remember that comparisons with null nullable operands return bool? not bool. Use HasValue check or lift the comparison with ?? to provide a fallback.
★ Nullable Type Quick Debug CommandsUse these commands to quickly check nullable state, avoid crashes, and diagnose silent bugs.
Accessing .Value throws InvalidOperationException
Immediate action
Stop the debugger and inspect the nullable's HasValue property.
Commands
Console.WriteLine($"HasValue: {myNullable.HasValue}");
Console.WriteLine($"Value or fallback: {myNullable ?? 0}");
Fix now
Replace .Value with ?? default or GetValueOrDefault(defaultValue).
Arithmetic with nullable produces unexpected null+
Immediate action
Breakpoint the line, hover over each operand, check HasValue.
Commands
var resolved = (nullableA ?? 0) + (nullableB ?? 0);
Console.WriteLine($"Operand A null? {nullableA is null}");
Fix now
Use ?? to provide defaults before calculation.
EF Core model gives default value for database NULL+
Immediate action
Check if property type is nullable (int?) or non-nullable (int).
Commands
Console.WriteLine($"DB value: {row.PropertyName?.ToString() ?? "null"}");
Review model class – use `?` suffix for nullable columns.
Fix now
Change property from int to int? to match database nullability.
Nullable vs Non-Nullable Value Types
Aspectint (non-nullable)int? / Nullable<int>
Can hold nullNo — compile errorYes — that's the whole point
Underlying typeValue type (struct)Value type (Nullable<T> 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

1
int? is syntactic sugar for Nullable<int>
a struct with HasValue (bool) and Value (int). It's still a value type, so there's no heap allocation.
2
Always use ?? or check HasValue before touching the underlying value
accessing .Value on a null nullable throws InvalidOperationException, not NullReferenceException.
3
Nullable arithmetic is silent
null + anything = null. Never let a nullable flow untested into a calculation that drives real logic or a database write.
4
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.
5
Pattern matching with 'is not null' is cleaner than HasValue
adopt it in new code.
6
EF Core maps nullable C# properties to nullable SQL columns
mismatch causes runtime exceptions.

Common mistakes to avoid

4 patterns
×

Directly casting a nullable to its base type without a null check

Symptom
Throws InvalidOperationException at runtime with message 'Nullable object must have a value' when the nullable is null.
Fix
Use playerScore ?? 0 for a safe fallback, or check playerScore.HasValue before accessing .Value.
×

Expecting nullable arithmetic to throw when an operand is null

Symptom
Expressions like nullableA + nullableB silently evaluate to null, producing wrong results (often empty strings in UI) with no error trace.
Fix
Always check HasValue or use ?? to resolve nullability before performing calculations that influence logic or output.
×

Using int? when a plain int with a sensible default is the right choice

Symptom
Code becomes harder to read and forces callers to handle null everywhere, even when null and zero have the same meaning.
Fix
Only reach for a nullable when null carries a distinct business meaning that cannot be expressed by any real value of the underlying type.
×

Forgetting that comparisons with nullable operands return bool? not bool

Symptom
A condition like if (nullableA < nullableB) does not compile because an implicit conversion from bool? to bool is not allowed. Developers often fix by casting, which can mask nulls.
Fix
Use HasValue checks before comparison, or resolve nulls with ?? to make the comparison non-nullable.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is Nullable in C#, and what are the two properties it exposes? H...
Q02SENIOR
What exception is thrown when you access .Value on a null nullable type,...
Q03SENIOR
Given int? a = null and int? b = 5, what is the result of a + b, and wha...
Q04SENIOR
How does pattern matching in C# 7+ handle nullable types differently fro...
Q01 of 04JUNIOR

What is Nullable in C#, and what are the two properties it exposes? How does it differ from a null reference on a class instance?

ANSWER
Nullable<T> is a struct that wraps a value type with a bool HasValue property and a T Value property. When HasValue is false, the nullable is considered null. Unlike a null reference on a class instance — which is a null pointer — a nullable struct is a valid struct whose HasValue is false. Accessing .Value when HasValue is false throws InvalidOperationException, not NullReferenceException, because the struct exists on the stack.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between int and int? in C#?
02
Can nullable types be used with all C# types?
03
Why does nullable arithmetic return null instead of throwing an exception?
04
How do I safely sum a nullable column with LINQ?
05
What's the difference between Nullable and C# 8 nullable reference types?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C# Basics. Mark it forged?

8 min read · try the examples if you haven't

Previous
File I/O in C#
9 / 11 · C# Basics
Next
Tuples in C#