Nullable Types: Handling Missing Values Safely – C# | Lesson 35
Introduction
In SchoolDb, many values can be missing. Meera was absent for the Science test, so her mark is NULL, and 21 of the 22 guardians have no email on record.
In SQL, you handled these with IS NULL, ISNULL, and COALESCE.
C# needs the same care. A missing value in C# is null, and using it carelessly causes
NullReferenceException, the most common crash in .NET applications. This lesson shows how
C# marks values that may be missing, and the operators that handle them safely.
By the end of this lesson, you will be able to:
- Use nullable value types such as
decimal?for SQL columns that allow NULL. - Explain nullable reference types and the compiler warnings they give.
- Use
?.,??,??=,is null, andis not null. - Explain why
!and.Valueare risky. - Avoid and fix
NullReferenceException.
Why this matters
A report card that crashes because one student was absent is not a small bug. It stops the whole class's report cards from printing, usually on the day before they go home.
Every NULL-able column you created in Module 3 will reach C# in Module 9. Knowing exactly how C# represents "no value" decides whether your code handles absent students gracefully or crashes on them.
The concept
Example: null is not zero
null means "no value": unknown, missing, or not applicable. It is not the same as 0 or an empty string:
| Value | Meaning in the SMS |
|---|---|
Mark 0 | The student wrote the test and scored nothing |
Mark null | The student was absent; there is no score |
Email "" | Someone saved an empty email, usually by mistake |
Email null | No email on record |
Think of a teacher's mark register. "0" and "AB" (absent) are written in different ways, because they mean different things. Averaging an "AB" as 0 would be unfair to the student.
Example: nullable value types
Value types, such as int, decimal, and DateOnly, always have a value: a plain
decimal cannot be null. Adding ? creates a nullable value type that can also be null:
| SchoolDb column | Allows NULL | C# type |
|---|---|---|
Marks.MarksObtained | Yes (absent) | decimal? |
BookIssues.ReturnedOn | Yes (not returned yet) | DateOnly? |
Students.DateOfBirth | No | DateOnly |
FeeInvoices.TotalAmount | No | decimal |
A decimal? has .HasValue (true or false) and .Value (the number). Reading .Value
when there is none throws an exception, so check HasValue or use the operators below.
Example: nullable reference types
Reference types, such as string, could always be null. Since C# 8, a project setting
lets the compiler help: with <Nullable>enable</Nullable>, which your project file has
since Lesson 30, string means "never null", and string? means "may be null".
| Declaration | Meaning | The compiler warns when you |
|---|---|---|
string name | Never null | Assign null to it |
string? email | May be null | Use it without checking |
These are warnings, not errors: the program still builds. Treat every one as a bug report that arrived early.
Example: the null operators
| Operator | Name | Example | Result when email is null |
|---|---|---|---|
?. | Null-conditional | email?.Length | null, instead of a crash |
?? | Null-coalescing | email ?? "(none)" | "(none)" |
??= | Null-coalescing assignment | occupation ??= "Not given" | Assigns only if null |
is null | Null check | if (email is null) | true |
is not null | Not-null check | if (mark is not null) | false |
?? is the C# version of SQL's COALESCE and ISNULL: the first value that is not null.
Apply it to the SMS
SchoolRules.cs gets a second GetGrade overload for marks that may be missing. It
returns string?, and null for an absent student, just as fn_GetGrade(NULL) returns
NULL in SQL:
Show src/SchoolApp.Console/SchoolRules.cs
// Lesson 34: school rules as reusable methods.
// A static class is a named container for methods; Lesson 37 explains classes in full.
static class SchoolRules
{
public const int PassMark = 33;
// Grade for a percentage: the same scale as fn_GetGrade in SQL (Lesson 21).
public static string GetGrade(decimal percentage)
{
return percentage switch
{
>= 91 => "A1",
>= 81 => "A2",
>= 71 => "B1",
>= 61 => "B2",
>= 51 => "C1",
>= 41 => "C2",
>= 33 => "D",
_ => "E"
};
}
// Lesson 35: an absent student has no mark (NULL), so no grade, like fn_GetGrade(NULL).
public static string? GetGrade(decimal? percentage)
{
if (percentage is null)
{
return null;
}
return GetGrade(percentage.Value);
}
// An expression-bodied method: a one-line method written with =>.
public static bool HasPassed(decimal percentage) => percentage >= PassMark;
// An optional parameter: maxMarks is 100 unless the caller says otherwise.
public static decimal CalculatePercentage(decimal marksObtained, decimal maxMarks = 100m)
{
return Math.Round(marksObtained / maxMarks * 100, 2);
}
// Two overloads: the same name, different parameters.
public static decimal CalculateAttendancePercentage(int attendedDays, int markedDays)
{
if (markedDays == 0)
{
return 0;
}
return Math.Round(100m * attendedDays / markedDays, 2);
}
public static decimal CalculateAttendancePercentage(string week)
{
int attended = 0;
foreach (char day in week)
{
if (day != 'A')
{
attended++;
}
}
return CalculateAttendancePercentage(attended, week.Length);
}
// The Try pattern: return true or false, and hand back results through out parameters.
public static bool TryParseAdmissionNumber(string text, out int year, out int serial)
{
year = 0;
serial = 0;
string[] parts = text.Split('-');
return parts.Length == 3
&& parts[0].Equals("ADM", StringComparison.OrdinalIgnoreCase)
&& int.TryParse(parts[1], out year)
&& int.TryParse(parts[2], out serial);
}
// ref: the method changes the caller's own variable.
public static void ApplyPayment(ref decimal balance, decimal amount)
{
balance -= amount;
}
// params: the caller can pass any number of marks.
public static decimal Average(params decimal[] marks)
{
if (marks.Length == 0)
{
return 0;
}
decimal total = 0;
foreach (decimal mark in marks)
{
total += mark;
}
return Math.Round(total / marks.Length, 2);
}
}
The program uses real NULLs from SchoolDb: the Science marks of Unit Test 1, and the guardians' emails and occupations.
Show src/SchoolApp.Console/Program.cs
// Lesson 35: nullable types, using values that can be NULL in SchoolDb.
// 1. Nullable value types: Meera was absent for Science, so her mark is NULL.
decimal? aaravScience = 88m;
decimal? meeraScience = null;
Console.WriteLine($"Aarav has a mark: {aaravScience.HasValue}, value {aaravScience.Value}");
Console.WriteLine($"Meera has a mark: {meeraScience.HasValue}");
Console.WriteLine($"Meera's mark or 0: {meeraScience.GetValueOrDefault()}");
// 2. The ?? operator gives a fallback when the value is null.
string meeraDisplay = meeraScience?.ToString() ?? "Absent";
Console.WriteLine($"Meera, Science: {meeraDisplay}");
// 3. The nullable GetGrade overload: no mark, no grade.
string aaravGrade = SchoolRules.GetGrade(aaravScience) ?? "-";
string meeraGrade = SchoolRules.GetGrade(meeraScience) ?? "-";
Console.WriteLine($"Grades: Aarav {aaravGrade}, Meera {meeraGrade}");
// 4. Averages must skip missing marks, as SQL's AVG skips NULL.
decimal?[] scienceMarks = { 88m, 91m, 72m, null, 61m, 83m };
decimal total = 0;
int counted = 0;
foreach (decimal? mark in scienceMarks)
{
if (mark is not null)
{
total += mark.Value;
counted++;
}
}
Console.WriteLine($"Science average of {counted} marks: {Math.Round(total / counted, 2)}");
// 5. Nullable reference types: Guardians.Email can be NULL.
string? rajeshEmail = "rajesh.sharma@nexcodingschool.example";
string? nehaEmail = null;
Console.WriteLine($"Rajesh's email length: {rajeshEmail?.Length}");
Console.WriteLine($"Neha's email length: {nehaEmail?.Length}"); // prints nothing
Console.WriteLine($"Neha's email: {nehaEmail ?? "(no email on record)"}");
// 6. ??= assigns only when the variable is still null.
string? kavitaOccupation = null;
kavitaOccupation ??= "Not given";
Console.WriteLine($"Kavita's occupation: {kavitaOccupation}");
// 7. is null and is not null: check before you use.
if (nehaEmail is null)
{
Console.WriteLine("Neha: send the fee reminder by phone instead of email");
}
Aarav has a mark: True, value 88
Meera has a mark: False
Meera's mark or 0: 0
Meera, Science: Absent
Grades: Aarav A2, Meera -
Science average of 5 marks: 79
Rajesh's email length: 37
Neha's email length:
Neha's email: (no email on record)
Kavita's occupation: Not given
Neha: send the fee reminder by phone instead of email
Example: the absent student
meeraScience is decimal? and holds null. HasValue is false, and
GetValueOrDefault() returns 0, the default for decimal.
meeraScience?.ToString() ?? "Absent" reads in two steps. ?. stops at null instead of
calling ToString(), and ?? then supplies "Absent". The report card shows a word, not a
crash and not a misleading 0.
Example: the nullable GetGrade overload
When the argument is a decimal?, the compiler picks the new overload. For Aarav's 88,
it returns "A2" through the original method. For Meera, it returns null, and ?? "-"
turns that into a dash.
The method's return type, string?, tells every caller: "this can be null, handle it".
That is the main idea of nullable reference types. The type itself documents the risk.
Example: averages skip missing marks
The six Science marks are 88, 91, 72, null, 61, and 83. The loop adds only marks that
are is not null, and divides by the number it actually counted:
| Calculation | Result |
|---|---|
| Five real marks: 395 ÷ 5 | 79, the correct class average |
| Treating Meera's null as 0: 395 ÷ 6 | 65.83, unfair to the class |
SQL's AVG skips NULL in the same way (Lesson 18). When you move a calculation from SQL
to C#, NULL handling is the first thing to check.
Example: the guardian with no email
Rajesh has an email of 37 characters. For Neha, nehaEmail?.Length gives null, and the
interpolated string prints nothing for it, so the line ends empty.
The last line shows the real-world response. When there is no email, the school phones instead. Handling null well is not only about avoiding crashes; it is about a sensible fallback for the person on the other side.
Example: what goes wrong without the checks
| Code | Compiler | When it runs |
|---|---|---|
string email = null; | warning CS8600: Converting null literal or possible null value to non-nullable type. | Builds |
nehaEmail.Length (null) | warning CS8602: Dereference of a possibly null reference. | NullReferenceException: Object reference not set to an instance of an object. |
meeraScience.Value (null) | warning CS8629: Nullable value type may be null. | InvalidOperationException: Nullable object must have a value. |
decimal mark = meeraScience; | error CS0266: Cannot implicitly convert type 'decimal?' to 'decimal'. | Does not build |
The warnings appear while you type, as yellow squiggles in the editor. Each one predicts a crash that would otherwise happen in front of a teacher.
Example: the ! operator
Writing nehaEmail!.Length silences warning CS8602. The ! is the null-forgiving
operator: it tells the compiler "trust me, this is not null".
The compiler stops warning, but the value is still null, and the program still crashes.
Use ! only when you truly know more than the compiler, and leave a comment explaining why.
Example: make nullable warnings errors
Warnings are easy to ignore when there are fifty of them. Many teams add one line to the
project file, inside <PropertyGroup>:
<WarningsAsErrors>nullable</WarningsAsErrors>
Now string email = null; stops the build with error CS8600 instead of a warning.
Nobody can merge code that might crash on a null. Turn it on at the start of a project;
switching it on in a large old project can produce hundreds of errors at once.
Example: NULL on its way from the database
In Module 9, C# reads rows from SchoolDb. Each data access tool handles NULL a little differently:
| Tool | A NULL column arrives as |
|---|---|
ADO.NET (SqlDataReader) | The special value DBNull.Value, which you must check for |
| Dapper | null, as long as your property is a ? type |
| EF Core | null, and scaffolding creates ? types for NULL-able columns |
In every case, the C# type must allow null. Otherwise, depending on the tool, the read fails, or worse, the NULL quietly becomes 0. The mapping table at the start of this lesson is the rule you will apply there.
Common mistakes
- Using
.Valuewithout checking. It throws when there is no value. Fix: checkHasValueoris not null, or use??andGetValueOrDefault(). - Treating null as 0 in averages. Absent students lower the class average. Fix:
skip nulls, like SQL's
AVG. - Ignoring nullable warnings. Each one is a possible
NullReferenceException. Fix: fix them, or turn warnings into errors in the project. - Silencing warnings with
!. The crash moves from compile time to run time. Fix: handle the null properly. - Using a plain type for a NULL-able column.
decimalcannot hold NULL, and the database value is lost or the read fails. Fix: match NULL-able columns with?types.
In real projects
The most common production error I have seen in .NET systems is NullReferenceException.
In one school system, the parent app crashed for every family whose guardian had no email,
about one parent in five.
The fix was two characters, ?. and ??, in one place. Finding it took a day, because
the crash happened far from where the null came from.
After that, the team turned on nullable reference types in every project and treated the warnings as errors. New null crashes almost stopped, because the compiler found them first.
Best practices
- Map every NULL-able column to a
?type, and every NOT NULL column to a plain type. - Keep
<Nullable>enable</Nullable>on, and fix every warning. - Use
??for display fallbacks and?.to stop at null safely. - Return
string?ordecimal?when "no result" is a normal outcome, and document it in the type. - Avoid
!and unchecked.Value.
Recap
- null means "no value"; it is not 0 and not an empty string.
decimal?,int?, andDateOnly?are value types that can also be null.- With nullable reference types,
stringnever holds null andstring?may. ?.stops at null,??supplies a fallback, and??=fills in only when null.- Averages must skip nulls, as SQL's
AVGdoes. - Nullable warnings predict
NullReferenceException; fix them instead of silencing them with!.
Homework
Library module task: Write a method that prints one book issue. Its return date is a
DateOnly?, because a book on loan has no return date yet.
Print "On loan" when the return date is null. Count days late up to the return date, or up to 28 July 2026 when the book is still on loan, and show the fine at 2 per day.
A correct result looks like:
Issue 1, Wings of Fire: Returned 2026-07-14; days late 0; fine 0
Issue 2, The Jungle Book: On loan; days late 8; fine 16
Show the solution
// Library homework, Lesson 35: book issues where the return date may be missing (NULL).
// Run with: dotnet run library-assignment/035-library-nullable.cs
DateOnly today = new DateOnly(2026, 7, 28);
PrintIssue(1, "Wings of Fire", new DateOnly(2026, 7, 15), new DateOnly(2026, 7, 14));
PrintIssue(2, "The Jungle Book", new DateOnly(2026, 7, 20), null);
void PrintIssue(int id, string title, DateOnly dueOn, DateOnly? returnedOn)
{
string status = returnedOn is null ? "On loan" : $"Returned {returnedOn.Value:yyyy-MM-dd}";
DateOnly checkDate = returnedOn ?? today; // not returned yet: count until today
int daysLate = Math.Max(0, checkDate.DayNumber - dueOn.DayNumber);
Console.WriteLine($"Issue {id}, {title}: {status}; days late {daysLate}; fine {daysLate * 2}");
}
returnedOn ?? today picks the return date when there is one, and today otherwise. The fine
of 16 for issue 2 matches the SQL result in Extra 1, using the same data.
Quiz
Which C# type matches a Marks.MarksObtained column that allows NULL?
Show answer
Answer: B. decimal? The ? lets the value type hold null for an absent student.
email is null. What does email ?? '(none)' return?
Show answer
Answer: C. (none) ?? returns the right side when the left side is null.
email is null. What does email?.Length return?
Show answer
Answer: B. null ?. stops at null and returns null instead of crashing.
Five marks are 88, 91, 72, 61, 83, and one is null. What is the correct average?
Show answer
Answer: B. 79 Skip the null: 395 divided by 5. Counting it as 0 gives 65.83.
What does the ! in nehaEmail!.Length do?
Show answer
Answer: B. Silences the compiler warning, but cannot prevent the crash It only tells the compiler to trust you; a null value still crashes.
Interview questions
Questions are grouped by experience level. Click a question to see a model answer. All questions: C# interview questions.
Fresher (0-2 yrs)
F What is a nullable value type? concept
A value type with ?, such as int? or decimal?, that can hold either a value or null.
It has HasValue and Value. It maps to SQL columns that allow NULL, such as a mark for an
absent student.
F What do the ?? and ?. operators do? concept
?? returns the left value unless it is null, then the right value, like SQL's COALESCE.
?. accesses a member only if the object is not null, and returns null otherwise, instead of
throwing NullReferenceException.
F What causes a NullReferenceException? concept
Using a member, such as .Length or a method, on a variable that holds null. The message is
"Object reference not set to an instance of an object." Check for null first, or use ?. and ??.
Mid (3-5 yrs)
M What are nullable reference types? concept
A compiler feature, turned on with <Nullable>enable</Nullable>, where string means "never
null" and string? means "may be null". The compiler warns (CS8600, CS8602, and others) when
code might use a null unsafely. It changes warnings, not run-time behavior.
M What does the null-forgiving operator ! do, and why is it risky? concept
x! tells the compiler to treat x as not null and silences the warning. It does nothing at
run time, so if x is null the program still crashes. Use it only when you truly know better
than the compiler, with a comment.
M How should NULL marks be handled when calculating a class average? coding
Skip them: add only marks that are not null and divide by how many were counted, as SQL's AVG
does. Treating an absent student's null as 0 lowers the average unfairly: 395 ÷ 5 is 79, but
395 ÷ 6 is 65.83.
Senior (6-9 yrs)
S The parent app crashes for about one family in five. The log shows NullReferenceException. How do you find and fix it? scenario
Read the stack trace for the exact line, then find which value is null and why, for example a
guardian with no email. Fix it where the value is used, with a null check and a sensible fallback,
and make the type string? so the compiler warns at every other use.
How to answer: Show the route from symptom to cause (stack trace, data pattern), the fix, and the prevention: nullable reference types on, warnings as errors, tests with missing data.
S How do you introduce nullable reference types into a large existing codebase? design
Enable it project by project or file by file (#nullable enable), starting with shared core
code. Fix warnings in small batches, annotate public APIs first, and turn warnings into errors
once a project is clean, so it stays clean.
How to answer: Emphasize a gradual, measurable rollout rather than enabling everything at once, and explain why public method signatures are the most valuable place to start.
Architect / Lead (10+ yrs)
A How should 'no value' flow consistently from the database through the API to the client? design
Decide per field: NULL-able columns map to ? types in C#, and the API either returns null in
JSON or omits the field, documented in the API contract. Never turn missing data into fake values
such as 0 or empty strings, which clients cannot tell apart from real data.
How to answer: Walk through each layer (database, data access, domain, API, client) and the rule at each. Mention consistency, documentation, and tests with missing data.
Resources
- Code for this lesson: git tag
lesson-35-nullable(code repository coming soon) - Files:
src/SchoolApp.Console/SchoolRules.cs,src/SchoolApp.Console/Program.cs,library-assignment/035-library-nullable.cs - Video: coming soon
What's next
You have now seen several exceptions: NullReferenceException, IndexOutOfRangeException,
and InvalidOperationException. Next, the last lesson of Module 6: catching exceptions,
throwing your own, and finding bugs with the debugger.
Lesson 36: Exceptions and debugging
Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.