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
usingSystem;
classNullableInternals
{
staticvoidMain()
{
// int? is just syntactic sugar for Nullable<int>// Both of these declarations are identical:
int? playerScore = null; // shorthand — what you'll write day-to-dayNullable<int> alsoPlayerScore = null; // longhand — what the compiler actually sees// HasValue tells you whether the nullable actually contains a numberConsole.WriteLine($"Has a score been set? {playerScore.HasValue}"); // False// Assign a real value
playerScore = 4200;
Console.WriteLine($"Has a score been set? {playerScore.HasValue}"); // TrueConsole.WriteLine($"The score is: {playerScore.Value}"); // 4200// DANGER ZONE: Accessing .Value when HasValue is false// throws InvalidOperationException, NOT NullReferenceExceptionint? 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 exceptionConsole.WriteLine($"Safe fallback score: {safeScore}"); // 0// You can also provide a custom defaultint 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
usingSystem;
// Simulates a database row for a customer accountclassCustomerAccount
{
publicstringName { 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
}
classRealWorldNullablePatterns
{
// Returns the display string for loyalty points// Without nullable types you'd use -1 or 0 as a sentinel — both are misleadingstaticstringGetPointsDisplay(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 existsstaticint? DaysSinceLastPurchase(CustomerAccount account)
{
// If LastPurchaseDate is null, this whole expression returns null — no crash// The cast to int? means the result can also be nullreturn (int?)(DateTime.Today - account.LastPurchaseDate)?.TotalDays;
}
// Applies a credit limit, but only if one hasn't been set yetstaticvoidEnsureCreditLimit(CustomerAccount account, decimal defaultLimit)
{
// ??= operator: only assigns if CreditLimit is currently null
account.CreditLimit ??= defaultLimit;
}
staticvoidMain()
{
var newCustomer = newCustomerAccount
{
Name = "Priya Sharma",
LoyaltyPoints = null, // never purchasedLastPurchaseDate = null, // no purchase historyCreditLimit = null // not yet assessed
};
var regularCustomer = newCustomerAccount
{
Name = "James Okafor",
LoyaltyPoints = 1850,
LastPurchaseDate = DateTime.Today.AddDays(-12),
CreditLimit = 500.00m
};
// ?? operator in actionConsole.WriteLine($"{newCustomer.Name}: {GetPointsDisplay(newCustomer)}");
Console.WriteLine($"{regularCustomer.Name}: {GetPointsDisplay(regularCustomer)}");
// null-conditional + ?? combo for safe navigationint? daysSinceNew = DaysSinceLastPurchase(newCustomer);
int? daysSinceRegular = DaysSinceLastPurchase(regularCustomer);
// ?? gives us a human-readable fallback when the value is nullConsole.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 nullEnsureCreditLimit(newCustomer, defaultLimit: 250.00m);
EnsureCreditLimit(regularCustomer, defaultLimit: 250.00m); // won't overwrite 500.00Console.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
usingSystem;
usingSystem.Collections.Generic;
usingSystem.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" tableclassOrder
{
public int OrderId { get; set; } // NOT NULL in DB — always present
public string CustomerEmail { get; set; } // NOT NULL — required to place an order
public DateTimeOrderPlacedAt { 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
}
classEntityFrameworkNullableMapping
{
// Simulates what EF Core would return from a database querystaticList<Order> GetSimulatedOrders()
{
returnnewList<Order>
{
newOrder
{
OrderId = 101,
CustomerEmail = "alice@example.com",
OrderPlacedAt = DateTime.Today.AddDays(-5),
ShippedAt = DateTime.Today.AddDays(-3),
DeliveredAt = DateTime.Today.AddDays(-1),
DiscountApplied = 15.00m
},
newOrder
{
OrderId = 102,
CustomerEmail = "bob@example.com",
OrderPlacedAt = DateTime.Today.AddDays(-2),
ShippedAt = DateTime.Today.AddDays(-1),
DeliveredAt = null, // not delivered yetDiscountApplied = null // no discount used
},
newOrder
{
OrderId = 103,
CustomerEmail = "carol@example.com",
OrderPlacedAt = DateTime.Today,
ShippedAt = null, // hasn't shipped yetDeliveredAt = null,
DiscountApplied = 5.00m
}
};
}
staticstringGetOrderStatus(Order order)
{
// Pattern matching on nullable types — clean and expressivereturn 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"
};
}
staticvoidMain()
{
var orders = GetSimulatedOrders();
foreach (var order in orders)
{
string status = GetOrderStatus(order);
// ?? makes the discount display clean without if/elsestring 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 needdecimal 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
usingSystem;
classPatternMatchingWithNullables
{
enumOrderStatus { Pending, Shipped, Delivered, Unknown }
// Returns order status using switch expression with property patternsstaticOrderStatusGetOrderStatus(Order order) => order switch
{
{ DeliveredAt: not null } => OrderStatus.Delivered,
{ ShippedAt: not null } => OrderStatus.Shipped,
_ => OrderStatus.Pending
};
staticstringDescribeDiscount(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}"
};
staticvoidMain()
{
var order1 = newOrder { ShippedAt = null, DeliveredAt = null };
var order2 = newOrder { ShippedAt = DateTime.Now, DeliveredAt = null };
var order3 = newOrder { 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 appliedConsole.WriteLine(DescribeDiscount(50m)); // Discount of £50.00Console.WriteLine(DescribeDiscount(150m)); // Generous discount!
}
}
// Simple Order class for demonstrationclassOrder
{
publicDateTime? ShippedAt { get; set; }
publicDateTime? 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
usingSystem;
classNullableMistakesAndFixes
{
staticvoidMain()
{
// ─────────────────────────────────────────────────────────// 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 Valueif (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 databaseint 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 misreadbool? 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 mathsbool 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.
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
Aspect
int (non-nullable)
int? / Nullable<int>
Can hold null
No — compile error
Yes — that's the whole point
Underlying type
Value type (struct)
Value type (Nullable<T> struct)
Memory size
4 bytes
5 bytes (4 + 1 bool for HasValue)
Exception on bad access
None — always has a value
InvalidOperationException if .Value accessed when HasValue is false
Default value
0
null (HasValue = false)
Maps to SQL nullable column
No — will throw on NULL data
Yes — maps cleanly to NULL
Arithmetic with null
N/A
Result is null — silent, not an exception
Works with pattern matching
Yes
Yes — including 'is not null' and 'is null' checks
Use when
The value must always exist
The 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.
Q02 of 04SENIOR
What exception is thrown when you access .Value on a null nullable type, and why is it that specific exception rather than a NullReferenceException?
ANSWER
InvalidOperationException is thrown with the message 'Nullable object must have a value.' It is not NullReferenceException because nullable types are value types (structs). There is no reference to dereference; the struct itself exists, but the internal Value field is not set. The runtime explicitly throws InvalidOperationException to indicate that the operation is invalid due to the state of the object.
Q03 of 04SENIOR
Given 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.)
ANSWER
a + b evaluates to null (of type int?). The lifted operator for addition returns null if any operand is null. a == b evaluates to false because the lifted equality operator returns false when one operand is null and the other is not — but if both were null, it would return true. Comparisons with nullables produce bool? when both are nullable, but equality is an exception: lifted == returns bool, not bool? when comparing two nullable<T>.
Q04 of 04SENIOR
How does pattern matching in C# 7+ handle nullable types differently from traditional HasValue checks? Give an example.
ANSWER
Pattern matching allows you to use 'is null' and 'is not null' directly on nullable types, making code more readable. For example, if (score is not null) is clearer than if (score.HasValue). In switch expressions, you can match on nullable properties: order switch { { DeliveredAt: not null } => "Delivered", ... }. This avoids accessing .Value entirely and integrates naturally with the rest of the pattern matching features.
01
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?
JUNIOR
02
What exception is thrown when you access .Value on a null nullable type, and why is it that specific exception rather than a NullReferenceException?
SENIOR
03
Given 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.)
SENIOR
04
How does pattern matching in C# 7+ handle nullable types differently from traditional HasValue checks? Give an example.
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
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<int>) 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.
Was this helpful?
02
Can nullable types be used with all C# types?
Nullable<T> 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<string> 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<T>.
Was this helpful?
03
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.
Was this helpful?
04
How do I safely sum a nullable column with LINQ?
Use the null-coalescing operator inside the Sum: orders.Sum(o => o.DiscountApplied ?? 0m). If the property is nullable, Sum expects a non-nullable selector, so you must provide a default. Alternatively, use the nullable overload by calling .Sum() on the nullable itself if it's already projected, but that returns a nullable result.
Was this helpful?
05
What's the difference between Nullable and C# 8 nullable reference types?
Nullable<T> is a struct that wraps value types to allow null. It exists at runtime and affects memory layout. Nullable reference types (string? annotation) are a compile-time feature that adds warnings and annotations but does not change the underlying reference type — a string? is still a reference that can be null at runtime. They are complementary but different mechanisms.