C# Nullable Types Explained — How, Why, and When to Use Them
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
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.
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 } }
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
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.
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}"); } }
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
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.
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}"); } }
Order #102 | Shipped — awaiting delivery | No discount
Order #103 | Processing | Discount: £5.00
Total discounts given: £20.00
Orders not yet shipped: 1
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'.
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}"); } }
[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
| Aspect | int (non-nullable) | int? / Nullable |
|---|---|---|
| Can hold null | No — compile error | Yes — that's the whole point |
| Underlying type | Value type (struct) | Value type (Nullable |
| 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
- int? is syntactic sugar for Nullable
— a struct with HasValue (bool) and Value (int). It's still a value type, so there's no heap allocation. - Always use ?? or check HasValue before touching the underlying value — accessing .Value on a null nullable throws InvalidOperationException, not NullReferenceException.
- Nullable arithmetic is silent: null + anything = null. Never let a nullable flow untested into a calculation that drives real logic or a database write.
- Only use nullable types when null has a genuine, distinct business meaning in your domain — if 'no value' and 'zero' mean the same thing, use the non-nullable type with a default value.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Directly casting a nullable to its value type without a null check — e.g. int score = (int)playerScore when playerScore is null — throws InvalidOperationException at runtime with the message 'Nullable object must have a value'. Fix: use playerScore ?? 0 for a safe fallback, or check playerScore.HasValue before accessing .Value.
- ✕Mistake 2: Expecting nullable arithmetic to throw when an operand is null — expressions like nullableA + nullableB silently evaluate to null instead of throwing. This produces wrong results downstream (often appearing as empty strings in the UI) with no error to trace. Fix: always check HasValue or use ?? to resolve nullability before performing calculations that influence logic or output.
- ✕Mistake 3: Using int? when a plain int with a sensible default is the right choice — overusing nullable types makes code harder to read and forces callers to handle null everywhere. Only reach for a nullable when null carries a distinct business meaning that cannot be expressed by any real value of the underlying type.
Interview Questions on This Topic
- QWhat is Nullable
in C#, and what are the two properties it exposes? How does it differ from a null reference on a class instance? - QWhat exception is thrown when you access .Value on a null nullable type, and why is it that specific exception rather than a NullReferenceException?
- QGiven int? a = null and int? b = 5, what is the result of a + b, and what type is that result? What would a == b evaluate to? (Tests understanding of lifted operators and three-valued logic in nullable comparisons.)
Frequently Asked Questions
What is the difference between int and int? in C#?
int is a value type that must always contain an integer. int? (shorthand for Nullable
Can nullable types be used with all C# types?
Nullable
Why does nullable arithmetic return null instead of throwing an exception?
C# implements 'lifted operators' for nullable types. When you apply an arithmetic operator like + or * to a nullable, and either operand is null, the result is null rather than an exception. This mirrors SQL's NULL arithmetic semantics and means unknown input produces unknown output. It's intentional but catches developers off guard because there's no runtime signal — always resolve null before arithmetic that matters.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.