Mid-level 4 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.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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.

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.
● 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?
🔥

That's C# Basics. Mark it forged?

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

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