Skip to content

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:

  1. Hallucinations: Plausible but factually incorrect data.
  2. Empty Responses: Zero-length strings or null values due to token limits or filtering.
  3. 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:

  1. Occupied: The package is inside.
  2. 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

  1. 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).
  2. 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, whereas string?? 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.

In this diagram, the Option pattern wraps uncertain AI inference results in a type that explicitly handles both presence and absence, contrasting with a naive approach that risks runtime null reference exceptions.
Hold "Ctrl" to enable pan & zoom

In this diagram, the `Option` pattern wraps uncertain AI inference results in a type that explicitly handles both presence and absence, contrasting with a naive approach that risks runtime null reference exceptions.

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:

  1. A valid recipe string: "Chicken Soup with extra ginger."
  2. An empty response: The model hallucinated a connection but returned nothing.
  3. 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

  1. Defining the Option<T> Class: We created a generic class Option<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 value T is technically null (e.g., T is a reference type), the Option container itself remains valid.

  2. Constructors and State Management: The class has two constructors. One accepts a value and sets _hasValue to true. The other (parameterless) sets _hasValue to false. This distinguishes between Option<string> holding an empty string "" (which has value) and Option<string> being None (which does not).

  3. The AIRecipeService Simulation: This class mimics an external AI dependency. Crucially, notice the return type Option<string>. Instead of returning string (which could be null), we return the wrapper. In a production system, the API adapter would wrap any raw null return immediately, ensuring the rest of the application never sees a raw null reference.

  4. The ProcessRecipe Handler: 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 access recipeOption.Value if HasValue is true, eliminating the risk of InvalidOperationException.

Visualization of Data Flow

The following diagram illustrates how the Option pattern protects the application flow from crashing.

Diagram: G
Hold "Ctrl" to enable pan & zoom

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 returns null, accessing result.HasValue throws a NullReferenceException.
  • 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 an InvalidOperationException.
  • Fix: Always gate access to .Value behind if (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 HasValue is true but Value is "", 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.