Chapter 10: Nullable Reference Types - Handling Hallucinations and Empty Responses
Theoretical Foundations
In the previous book, we established the foundational object-oriented principles, including the interface contract which allows for polymorphic behavior. We will now extend that foundation to address a critical flaw in many systems: the assumption that data is always present and valid. In AI systems, this assumption is a primary source of runtime instability.
The Illusion of Certainty in Probabilistic Systems
Traditional software engineering often relies on the "happy path," assuming that database queries return rows, network requests succeed, and objects are fully initialized. However, AI models operate in a probabilistic domain. When an AI generates a response, it may produce:
- Hallucinations: Plausible but factually incorrect data.
- Empty Responses: Zero-length strings or null values due to token limits or filtering.
- Incomplete Objects: A data structure where required fields are missing.
Treating these uncertain states as concrete values leads to NullReferenceException crashes. To build robust AI architectures, we must explicitly model the concept of "absence" or "uncertainty."
The Option/Maybe Pattern
The core theoretical concept introduced here is the Option/Maybe pattern. This is a generic wrapper type that encapsulates a value that may or may not exist. Instead of returning a raw reference type (which could be null), we return an instance of Option<T>.
This pattern forces the developer to acknowledge the possibility of absence at the compile-time level, rather than deferring it to a runtime crash.
Real-World Analogy: The Secure P.O. Box
Imagine a postal system that guarantees delivery. If a package is delivered, it is placed in a secure locker. The locker has two states:
- Occupied: The package is inside.
- Empty: The locker is vacant.
If you go to the locker expecting a package, you cannot blindly reach inside; you must first check if the locker is open (occupied) or locked (empty). The Option<T> pattern is this locker system. It wraps the "package" (the data) and provides a mechanism to safely check its state before accessing it.
Implementing the Generic Option Type
We will implement a simplified Option<T> struct. Using a struct (value type) can reduce heap allocations, which is critical when processing high volumes of AI inference requests.
using System;
namespace AI.DataStructures
{
// A generic struct representing a value that may or may not exist.
// This prevents the use of null references for optional data.
public struct Option<T>
{
private readonly T _value;
private readonly bool _hasValue;
// Private constructor to enforce creation via static methods
private Option(T value, bool hasValue)
{
_value = value;
_hasValue = hasValue;
}
// Factory method to create a Some (present) state
public static Option<T> Some(T value)
{
if (value == null)
throw new InvalidOperationException("Cannot wrap a null value in Option<T>. Use None instead.");
return new Option<T>(value, true);
}
// Factory method to create a None (absent) state
public static Option<T> None()
{
return new Option<T>(default(T), false);
}
// Property to check existence
public bool HasValue => _hasValue;
// Unsafe accessor - throws if value is missing
public T Value
{
get
{
if (!_hasValue) throw new InvalidOperationException("Option has no value.");
return _value;
}
}
// Safe retrieval with fallback
public T GetValueOrDefault(T defaultValue)
{
return _hasValue ? _value : defaultValue;
}
}
}
Architectural Implications for AI Systems
In the context of AI development, specifically when interfacing with Large Language Models (LLMs), the Option<T> pattern serves as a defensive boundary. Consider a scenario where we query an AI for a structured entity, such as a Person object.
1. Handling Hallucinations as "None"
If an AI generates text that cannot be parsed into our expected Person schema, we should not return a Person object with default/empty values (which implies validity). Instead, we return Option<Person>.None(). This signals to the consuming system that the AI failed to produce a valid entity.
2. The Chain of Responsibility
When building a pipeline of AI agents (e.g., a Planner -> Executor -> Verifier), the output of one agent becomes the input of the next. If the Planner returns an empty plan, the Executor should not attempt to execute null. It should check the Option state.
public class AIPlanner
{
// Returns an Option, explicitly acknowledging the possibility of failure
public Option<ExecutionPlan> GeneratePlan(string prompt)
{
// Simulate AI inference
string rawOutput = CallLLM(prompt);
if (string.IsNullOrWhiteSpace(rawOutput))
{
return Option<ExecutionPlan>.None();
}
try
{
var plan = ParsePlan(rawOutput);
return Option<ExecutionPlan>.Some(plan);
}
catch (FormatException)
{
// The AI hallucinated a format we don't understand
return Option<ExecutionPlan>.None();
}
}
private string CallLLM(string prompt) { /* ... */ return ""; }
private ExecutionPlan ParsePlan(string raw) { /* ... */ return new ExecutionPlan(); }
}
public class AIExecutor
{
public void Execute(Option<ExecutionPlan> planOption)
{
// We are forced to handle the None case explicitly
if (!planOption.HasValue)
{
Console.WriteLine("No valid plan generated. Aborting execution.");
return;
}
// Accessing .Value is safe here because we checked HasValue
ExecutionPlan plan = planOption.Value;
Console.WriteLine($"Executing plan: {plan.Name}");
}
}
Static Analysis and Nullable Reference Types
While Option<T> is a design pattern, C# 8.0+ introduced Nullable Reference Types as a language feature to enforce null safety at the compiler level. This is distinct from the Option<T> pattern but serves the same goal: eliminating null reference exceptions.
When Nullable Reference Types are enabled (via <Nullable>enable</Nullable> in the project file), the compiler treats all reference types as non-nullable by default. To allow a null, you must explicitly annotate it with ?.
#nullable enable
public class AIResponse
{
// This string CANNOT be null (compiler enforced)
public string Content { get; set; }
// This string CAN be null
public string? ErrorMessage { get; set; }
public AIResponse(string content)
{
this.Content = content; // Required
this.ErrorMessage = null; // Optional
}
}
Comparison: Option vs. Nullable Reference Types
- Nullable Reference Types are best for simple reference types (strings, classes) where the absence of a value is semantically meaningful (e.g., an optional error message).
- Option
is best for complex scenarios, especially with Value Types (structs), and for creating explicit domain models. It also allows for richer composition (e.g., Option<Option<T>>is valid and distinct, whereasstring??is redundant).
In high-stakes AI systems, we often use both. We use nullable annotations for simple properties and the Option<T> pattern for return types of complex operations (like AI inference) to make the "uncertainty" a first-class citizen of our type system.
Visualizing the Data Flow
The following diagram illustrates how an uncertain AI response flows through a system using the Option pattern, contrasting it with a naive null-based approach.
Theoretical Foundations
By utilizing Generics, we create a reusable Option<T> container. This container abstracts the concept of "nothingness." In AI systems, where inputs and outputs are inherently volatile, this abstraction provides a compile-time guarantee of safety. It transforms runtime exceptions into compile-time logical checks, ensuring that an empty response from a model results in a controlled fallback strategy rather than a system crash.
Basic Code Example
Scenario: AI-Generated Recipe Assistant
Imagine an AI-powered cooking assistant. A user asks, "What's a good recipe for a rainy day?" The AI model might generate a response. However, the response could be:
- A valid recipe string:
"Chicken Soup with extra ginger." - An empty response: The model hallucinated a connection but returned nothing.
- A null reference: The API call timed out or the specific context was lost.
In a traditional system, accessing the result of a null response (e.g., result.Length) causes a NullReferenceException, crashing the application. To handle this, we implement the Option/Maybe pattern. This pattern wraps a value that may or may not be present, forcing the developer to handle the "empty" case explicitly.
Code Example: The Option<T> Pattern
Here is a basic implementation of a generic Option container. This example uses Generics (allowed) but avoids Lambda expressions and LINQ (forbidden).
using System;
namespace AI_Cooking_Assistant
{
// 1. The Option Container
// Represents a value that may or may not be present.
// 'T' is the type of the value (e.g., string, Recipe).
public class Option<T>
{
private readonly T _value;
private readonly bool _hasValue;
// Constructor for a value that exists
public Option(T value)
{
_value = value;
_hasValue = true;
}
// Constructor for an empty state (None)
public Option()
{
_value = default(T); // Default value for type T
_hasValue = false;
}
// Helper property to check existence
public bool HasValue => _hasValue;
// Helper property to safely retrieve value
public T Value
{
get
{
if (!_hasValue)
throw new InvalidOperationException("Cannot access Value on an empty Option.");
return _value;
}
}
}
// 2. Static Factory for cleaner syntax
public static class Option
{
public static Option<T> Some<T>(T value) => new Option<T>(value);
public static Option<T> None<T>() => new Option<T>();
}
// 3. The AI Service Simulation
public class AIRecipeService
{
// Simulates an API call that might return null or empty
public Option<string> GetRecipeSuggestion(string weather)
{
if (weather == "Rainy")
{
// Simulate successful generation
return Option.Some("Chicken Soup with extra ginger");
}
else if (weather == "Storm")
{
// Simulate an empty response (hallucination resulted in nothing)
return Option.None<string>();
}
else
{
// Simulate a null reference / timeout
return null; // This is a potential failure point we need to handle
}
}
}
// 4. Main Execution
class Program
{
static void Main(string[] args)
{
AIRecipeService service = new AIRecipeService();
// Case 1: Valid Response
Option<string> rainyRecipe = service.GetRecipeSuggestion("Rainy");
ProcessRecipe(rainyRecipe);
// Case 2: Empty Response (Hallucination)
Option<string> stormRecipe = service.GetRecipeSuggestion("Storm");
ProcessRecipe(stormRecipe);
// Case 3: Null Response (Timeout)
// Note: In a strictly typed system, we might wrap this return value
// immediately in an Option, but for this example, we assume the
// service might return null directly.
Option<string> sunnyRecipe = service.GetRecipeSuggestion("Sunny");
ProcessRecipe(sunnyRecipe);
}
// The handler logic that prevents crashes
static void ProcessRecipe(Option<string> recipeOption)
{
// Defensive check: Is the container itself null?
if (recipeOption == null)
{
Console.WriteLine("Error: The service returned a null reference.");
return;
}
// Check if the container holds a value
if (recipeOption.HasValue)
{
Console.WriteLine($"Success: Here is your recipe -> {recipeOption.Value}");
}
else
{
Console.WriteLine("Notice: The AI generated an empty response. Please try rephrasing.");
}
}
}
}
Step-by-Step Explanation
-
Defining the
Option<T>Class: We created a generic classOption<T>. This allows us to wrap any data type (strings, integers, complex objects). It contains a private boolean flag_hasValue. This flag is the source of truth. Even if the wrapped valueTis technically null (e.g.,Tis a reference type), theOptioncontainer itself remains valid. -
Constructors and State Management: The class has two constructors. One accepts a value and sets
_hasValueto true. The other (parameterless) sets_hasValueto false. This distinguishes betweenOption<string>holding an empty string""(which has value) andOption<string>beingNone(which does not). -
The
AIRecipeServiceSimulation: This class mimics an external AI dependency. Crucially, notice the return typeOption<string>. Instead of returningstring(which could benull), we return the wrapper. In a production system, the API adapter would wrap any rawnullreturn immediately, ensuring the rest of the application never sees a raw null reference. -
The
ProcessRecipeHandler: This is where the safety is enforced. We perform two checks:- Null Check:
if (recipeOption == null)handles the case where the service failed to wrap the result (a fallback). - HasValue Check:
if (recipeOption.HasValue)safely unwraps the data. We only accessrecipeOption.ValueifHasValueis true, eliminating the risk ofInvalidOperationException.
- Null Check:
Visualization of Data Flow
The following diagram illustrates how the Option pattern protects the application flow from crashing.
Common Pitfalls
1. Forgetting to Check the Container Nullity
While Option<T> is designed to prevent nulls, it is possible for a poorly designed API to return a literal null instead of an Option.None<T>().
- Mistake: Assuming
Option<string> result = service.Call();is never null. - Consequence: If
service.Call()fails and returnsnull, accessingresult.HasValuethrows aNullReferenceException. - Fix: Always perform a preliminary null check on the container itself before accessing its properties, or enforce strict interface contracts that return
Option<T>(non-nullable).
2. Accessing .Value without Checking .HasValue
The Option class is a guardrail, not a magic force field. If you bypass the safety check, you introduce risk.
- Mistake:
Console.WriteLine(recipeOption.Value); - Consequence: If the option is empty (
None), this throws anInvalidOperationException. - Fix: Always gate access to
.Valuebehindif (recipeOption.HasValue).
3. Confusing None with Empty Strings
In the context of AI hallucinations, an empty string "" is distinct from a None state.
- Empty String (
""): The AI generated text, but the text happened to be empty (rare, but possible). This is a valid value. - None: The AI failed to generate text, or the generation process crashed.
- Fix: Ensure your logic distinguishes between these. If
HasValueis true butValueis"", you might want to treat it as a "low quality" response rather than a total failure.
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.