Nullable Types in C#

Nullable types in C# are used when a variable must represent either a real value or no value. This matters because real software regularly receives incomplete data. A database field may be empty, a form field may be optional, an API response may omit a property, or a calculation may not have a result yet.

C# has two important nullable concepts. Nullable value types, such as int?, bool?, and DateTime?, allow value types to store null. Nullable reference types, such as string?, help the compiler warn when a reference may be null. The syntax is similar, but the behavior and purpose are different.


What Are Nullable Types in C#?

A nullable type is a type that can hold a normal value or null. Normal value types cannot hold null. An int must contain an integer, a bool must contain true or false, and a DateTime must contain a date and time value. Nullable value types add one extra state: no value.

int age = 25;
int? optionalAge = null;

bool isActive = true;
bool? emailVerified = null;

The question mark after the type means null is allowed. Internally, int? is shorthand for Nullable<int>. This is a generic structure provided by .NET for nullable value types.

Why Nullable Types Are Needed

Without nullable types, developers often use fake values to represent missing data. For example, 0 may be used for an unknown age, -1 for an invalid ID, or DateTime.MinValue for a missing date. This creates confusion because those fake values can accidentally be treated as real values.

Nullable types make the meaning direct. If int? is null, the value is missing. If it contains 0, the value is actually zero. This difference is important in reports, forms, database models, financial systems, and API contracts.

Use null only when absence is a meaningful state in the problem you are modeling.

Nullable Value Type Syntax

The short syntax is to add ? after a value type. It works with built-in value types, enums, and custom structs. The variable can then store a normal value or null.

int? quantity = null;
double? temperature = 36.5;
DateTime? deliveryDate = null;
OrderStatus? status = OrderStatus.Pending;

You can assign a normal value to a nullable variable, and you can assign null. The compiler allows both because the nullable wrapper is designed to represent these two states.

HasValue and Value

Nullable value types provide HasValue and Value. HasValue tells whether a real value exists. Value returns that value, but it must be used only when HasValue is true.

int? marks = 82;

if (marks.HasValue)
{
    Console.WriteLine(marks.Value);
}
else
{
    Console.WriteLine("Marks not available");
}

If you access Value when the nullable variable is null, C# throws an InvalidOperationException. For this reason, many developers prefer safer operators such as ??, ?., pattern matching, or GetValueOrDefault().

Null-Coalescing Operator

The null-coalescing operator ?? provides a fallback value when the nullable value is null. It keeps code readable when a default value makes sense.

int? discount = null;
int finalDiscount = discount ?? 0;

Console.WriteLine(finalDiscount);

Here, finalDiscount becomes 0 because discount is null. If discount contained 10, that value would be used instead. The fallback should always match the business meaning of missing data.

Nullable Reference Types

Nullable reference types were added to reduce null reference bugs. Before this feature, reference types such as string, arrays, classes, and interfaces could always be null, even when code did not clearly say so. That made NullReferenceException one of the most common runtime errors in C# applications.

With nullable reference types enabled, string means the value should not be null, while string? means null is allowed. The compiler then gives warnings when a possibly null value is used without a check.

string name = "Nerds Do Stuff";
string? nickname = null;

Console.WriteLine(name.Length);
Console.WriteLine(nickname?.Length);

This feature does not change reference type runtime behavior. It mainly improves compile-time analysis. The goal is to make nullability part of the code contract so developers know which values must exist and which values need checking.

Nullable Value Types vs Nullable Reference Types

FeatureNullable Value TypeNullable Reference Type
Exampleint?string?
PurposeAllows value types to hold nullWarns about possible null references
Runtime behaviorUses Nullable<T>Reference can still be null
Main benefitRepresents missing numeric or struct valuesReduces null reference bugs

The syntax looks similar, but the meaning is not identical. Nullable value types create a real nullable wrapper. Nullable reference types create annotations and compiler warnings that help you write safer code.

GetValueOrDefault Method

GetValueOrDefault() returns the stored value if it exists. If the nullable value is null, it returns the default value of the underlying type. For int, the default is 0. For bool, it is false. You can also pass a custom default value.

int? stock = null;

Console.WriteLine(stock.GetValueOrDefault());
Console.WriteLine(stock.GetValueOrDefault(100));

Null-Conditional Operator

The null-conditional operator ?. lets you access a member only when the value is not null. If the value is null, the expression returns null instead of throwing an exception. This is useful when object chains contain optional values.

User? user = GetUser();
int? nameLength = user?.Name?.Length;

In this example, user?.Name?.Length is safe even if user is null or Name is null. The result becomes null instead of crashing the program. This is clean, but it should not be used to hide required data. If a value must exist for the program to work correctly, validate it directly and fail clearly.

Null-Coalescing Assignment

The null-coalescing assignment operator ??= assigns a value only when the variable is currently null. It is useful for lazy initialization, default configuration, and optional collections.

List<string>? names = null;

names ??= new List<string>();
names.Add("CSharp");

The list is created only if names is null. If it already contains an existing list, that list is preserved. This avoids overwriting data accidentally while keeping initialization compact.

Pattern Matching with Nullable Values

Pattern matching is another clean way to handle nullable values. Instead of checking HasValue and then reading Value, you can check whether the nullable value contains a value and capture it into a normal variable.

int? score = 91;

if (score is int actualScore)
{
    Console.WriteLine(actualScore);
}

Inside the if block, actualScore is a normal int. This style is readable because the null check and value extraction happen in one expression.

Nullable Types with Databases and APIs

Nullable types are especially important when working with databases and APIs. Database columns often allow null, and API payloads often contain optional fields. If a database column named MiddleName allows null, the C# model should usually represent it as string?. If a column named DeliveryDate can be empty, the model should usually use DateTime?.

This keeps the code honest. The model tells developers which values are required and which values may be missing. It also prevents fake defaults from spreading into business logic. A missing delivery date is not the same thing as today, and an unknown price is not the same thing as zero.

When Not to Use Nullable Types

Nullable types should not be used everywhere. If a value is required for a valid object, make it non-nullable and require it during construction or validation. Overusing nullable types makes code noisy because every caller must keep checking for null, even when the value should logically exist.

A good rule is to make invalid states difficult to create. If a customer must always have an email address, the model should enforce that rule. If the email address is optional, then string? is correct. Nullability should describe the real business rule, not developer uncertainty.

Nullable Types in Method Parameters

Nullable annotations are also useful in method parameters and return types. A parameter like string name tells the caller that the method expects a real string. A parameter like string? name tells the caller that null is accepted and handled. This makes method contracts clearer before anyone reads the implementation.

void PrintUserName(string? name)
{
    if (string.IsNullOrWhiteSpace(name))
    {
        Console.WriteLine("Guest");
        return;
    }

    Console.WriteLine(name);
}

Return types work the same way. If a method can fail to find a record, returning User? is clearer than returning a fake empty object. The caller is then forced by compiler warnings to handle the missing value path.

For large projects, nullable annotations also improve teamwork. They document intent directly in the type system, so another developer can immediately see whether null is expected. This reduces guesswork during maintenance and makes refactoring safer because the compiler highlights places where null handling may be incomplete.

The best nullable code is explicit: required values stay non-nullable, optional values are marked nullable, and every missing-value path is handled intentionally before the value reaches critical business logic.

That discipline prevents avoidable runtime failures.

Common Mistakes with Nullable Types

  • Accessing Value without checking HasValue.
  • Using null when a normal default value would be clearer.
  • Ignoring nullable reference type warnings instead of fixing the flow.
  • Using the null-forgiving operator without proof that the value cannot be null.
  • Letting nullable values spread through the entire application instead of validating them at input boundaries.

Best Practices for Nullable Types in C#

Use nullable types when absence is meaningful. Avoid null when a value is always required. Check nullable values before using them, enable nullable reference types in modern projects, and treat compiler warnings as useful feedback instead of noise.

Keep null handling near input boundaries such as APIs, databases, configuration files, and user forms. Once data is validated, convert it into strong models with clear rules. This makes the core logic cleaner and reduces defensive null checks everywhere.