Skip to content

Chapter 19: Error Handling and ProblemDetails

Theoretical Foundations

In the landscape of modern distributed systems, particularly when orchestrating AI services, the reliability of communication between client and server is paramount. When an AI model encounters an input that violates safety guidelines, or when a downstream vector database connection times out, the client application—whether a mobile app or a single-page application—needs a predictable, structured way to understand what went wrong. This is the domain of Error Handling and ProblemDetails, a critical architectural layer that transforms chaotic exceptions into standardized, machine-readable diagnostics.

To understand the necessity of this standardization, we must first appreciate the chaos of unstructured error handling. In legacy systems, an unhandled exception might result in a raw 500 Internal Server Error with a stack trace dumped into the HTML body, or worse, a plain text string like "Model not found." For an AI application consuming an API, this is catastrophic. If an AI service returns a vague error, the client cannot distinguish between a transient network failure (retryable), a billing limit (actionable by the user), or a content safety violation (requiring prompt modification). Without a contract, the client is blind.

The solution lies in RFC 7807 (Problem Details for HTTP APIs). This specification defines a standard JSON format for reporting application-level errors. It separates the HTTP status code (the "what happened at the transport level") from the semantic error details (the "what happened at the application level").

The Anatomy of a Problem Detail

A Problem Detail object is a structured map of information. It contains:

  1. type: A URI reference that identifies the problem type (e.g., https://api.myapp.com/errors/out-of-credits). This is the most critical field for programmatic handling.
  2. title: A short, human-readable summary of the problem.
  3. status: The HTTP status code (e.g., 429 for Too Many Requests).
  4. detail: A specific explanation for this occurrence (e.g., "Your request exceeded the token limit for model 'gpt-4-turbo'").
  5. instance: A URI reference to the specific request that caused the error.
  6. extensions: Arbitrary additional properties specific to the domain (e.g., retryAfterSeconds for rate limiting).

In an AI context, imagine a client sending a prompt to an image generation endpoint. If the prompt violates content policies, returning a standard 400 Bad Request is insufficient. A Problem Detail response might look like this:

{
    "type": "https://api.example.com/errors/content-filter",
    "title": "Content Safety Violation",
    "status": 400,
    "detail": "The prompt contains prohibited content regarding violence.",
    "violatedPolicy": "violence",
    "suggestedAction": "Rewrite prompt to remove explicit imagery"
}

This structure allows the client to parse the error programmatically. The client can check the type URI and route the error to a specific UI handler (e.g., showing a "Safety Warning" modal) rather than a generic "Something went wrong" toast notification.

The Middleware Pipeline: The Conveyor Belt Analogy

To understand how we achieve this in ASP.NET Core, visualize the HTTP request pipeline as a conveyor belt in a high-tech packaging facility.

  1. The Request (The Item): A client sends an HTTP request. This item enters the facility (the API).
  2. Standard Middleware (The Initial Sorters): The request passes through standard middleware (Authentication, Routing). These are like robotic arms sorting items onto the correct production line.
  3. The Controller (The Packaging Station): The request reaches the controller action. This is where the AI model is invoked. The item is being processed.
  4. The Exception (The Jam): Suddenly, the machinery jams. The AI model throws a ContentFilteredException. In a naive implementation, this jam stops the belt and throws a wrench (a raw stack trace) at the client.
  5. The Error Handling Middleware (The Safety Net): We install a specialized net under the conveyor belt. This net catches the wrench (exception), inspects it, and repackages it into a standardized box (Problem Detail) before sending it down the line to the client.

In ASP.NET Core, this "Safety Net" is implemented using the IExceptionHandler interface and the ProblemDetails service.

The IExceptionHandler Interface

Introduced in recent versions of .NET, IExceptionHandler provides a formalized contract for handling exceptions globally. Unlike the older UseExceptionHandler which relied on inline lambda configurations, IExceptionHandler allows us to register multiple handlers that form a chain of responsibility.

This is crucial for AI APIs where different exceptions require different logic:

  • Validation Exceptions: Should result in a 422 Unprocessable Entity.
  • AI Service Exceptions (e.g., OpenAI API errors): Should be mapped to 502 Bad Gateway (since the upstream service failed).
  • Content Filter Exceptions: Should result in a 400 Bad Request with specific metadata.

By implementing IExceptionHandler, we decouple the detection of an error from its classification and serialization. Each handler asks: "Can I handle this exception type?" If yes, it generates a ProblemDetails context; if no, it passes the exception to the next handler in the chain.

The AddProblemDetails Service

While IExceptionHandler catches the exceptions, the AddProblemDetails extension method configures the dependency injection container to understand the ProblemDetails type. It registers services that allow us to inject IProblemDetailsService into our handlers.

This service is responsible for the final serialization. It ensures that every error response adheres to the RFC 7807 structure, regardless of where it originated. It also handles content negotiation, ensuring that if a client requests application/xml, the Problem Details are serialized accordingly (though JSON is standard).

Visualizing the Error Handling Flow

The following diagram illustrates the flow of an exception through the IExceptionHandler pipeline, contrasting it with the standard request flow.

This diagram visually contrasts the standard request flow with the exception handling pipeline, showing how the IExceptionHandler intercepts errors and performs content negotiation to serialize Problem Details into the format requested by the client (such as XML).
Hold "Ctrl" to enable pan & zoom

This diagram visually contrasts the standard request flow with the exception handling pipeline, showing how the `IExceptionHandler` intercepts errors and performs content negotiation to serialize Problem Details into the format requested by the client (such as XML).

Handling AI-Specific Failures

In the context of AI Web APIs, error handling is not merely about catching bugs; it is about interpreting the behavior of non-deterministic systems. Consider the integration with an AI service like Azure OpenAI or a local Llama model.

  1. Content Filtering: Modern AI services often have built-in safety filters. If a prompt triggers these, the service might return a specific error code or simply refuse to generate output. We need a custom IExceptionHandler that detects this specific signal and translates it into a Problem Detail with a type of https://api.example.com/errors/content-violation. This allows the client to inform the user exactly why their request was rejected, perhaps suggesting a safer alternative.
  2. Rate Limiting (Token Bucket): AI APIs are expensive. We often implement rate limiting (as discussed in previous chapters on Middleware). If a user exceeds their token limit, we shouldn't just throw a 429 Too Many Requests. We should return a Problem Detail that includes a retryAfter field. The client can use this to disable the "Send" button for a specific duration.
  3. Model Unavailable: If we are routing requests between multiple models (e.g., fallback from GPT-4 to GPT-3.5), and the primary model fails, we need to distinguish between a temporary outage and a configuration error. A IExceptionHandler can inspect the exception message or inner exceptions to determine if the failure is transient (suggesting a retry) or permanent (suggesting a fallback to a different model).

The Role of Validation Middleware

While IExceptionHandler deals with exceptions (unexpected errors), we also need to handle validation errors (expected errors). In ASP.NET Core, when model binding fails or validation attributes (like [Required] or [StringLength]) are violated, the framework automatically short-circuits the request and returns a 400 Bad Request.

However, the default format of these validation errors (a dictionary of errors keyed by property name) is not RFC 7807 compliant. To standardize this, we often create custom middleware or override the built-in invalid model state behavior.

This custom middleware intercepts the ModelState before it reaches the controller. It iterates through the errors, constructs a ProblemDetails object with status = 400 or 422, and populates the extensions with details about which fields failed validation. This ensures that whether the error is a code-level exception or a data-level validation failure, the client receives a consistent JSON structure.

Architectural Implications for AI Systems

Adopting this rigorous error handling strategy has profound implications for the architecture of AI systems:

  • Observability: By standardizing errors, we can easily aggregate logs. We can query our logs for all occurrences of type: "https://api.example.com/errors/rate-limit" to understand user behavior, or type: "https://api.example.com/errors/token-length" to see if our context windows are too small.
  • Client Resilience: Clients built against a Problem Details contract can implement sophisticated retry logic. A client knows that a 500 error might be transient, but a 400 with type content-violation is permanent and should not be retried.
  • Microservices Communication: If your AI API acts as a client to other microservices (e.g., a User Profile service or a Billing service), and those services return Problem Details, your AI API can bubble up those errors transparently. The end-user sees a unified error format regardless of which internal service failed.

Summary of Concepts

In summary, the "Error Handling and ProblemDetails" chapter establishes a contract of clarity. It moves error handling from an afterthought to a first-class architectural concern. By leveraging IExceptionHandler and AddProblemDetails, we build a defensive layer that:

  1. Catches exceptions anywhere in the pipeline.
  2. Classifies them based on domain logic (AI specific vs. generic).
  3. Serializes them into a standard, machine-readable format (RFC 7807).

This ensures that our AI Web API is not just a black box that occasionally fails, but a robust, communicative service that provides actionable feedback to its consumers.

Basic Code Example

Here is a self-contained, "Hello World" level example demonstrating how to implement global exception handling and standardized ProblemDetails responses in an ASP.NET Core Web API.

Real-World Context

Imagine you are building an AI-powered image generation API. A client application sends a request to generate an image. During processing, the AI service encounters an unexpected error (e.g., a null reference when processing the prompt). Without a standardized error handling mechanism, the API might return a generic HTML error page or an inconsistent JSON structure. This forces the client developer to write fragile parsing logic. By implementing IExceptionHandler and ProblemDetails, we ensure that every error—whether a database failure or a logic bug—returns a predictable, machine-readable JSON response adhering to RFC 7807, allowing the client to display a user-friendly error message instantly.

Code Example

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

// 1. Configure the ProblemDetails service
// This enables RFC 7807 compliant error responses.
builder.Services.AddProblemDetails();

// 2. Register the Exception Handler
// This middleware intercepts unhandled exceptions before they reach the response stage.
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

// 3. Enable the Exception Handling Middleware
// This activates the pipeline defined in AddExceptionHandler.
app.UseExceptionHandler();

// A simple endpoint that simulates an AI processing failure
app.MapGet("/generate-image", (string prompt) =>
{
    // Simulate a logic error: The AI model requires a non-empty prompt
    if (string.IsNullOrWhiteSpace(prompt))
    {
        throw new InvalidOperationException("The AI prompt cannot be empty.");
    }

    // Simulate a critical infrastructure failure (e.g., GPU memory error)
    if (prompt.Contains("complex"))
    {
        throw new OutOfMemoryException("GPU memory exhausted processing complex prompt.");
    }

    return Results.Ok(new { ImageUrl = $"https://ai.service/image/{Guid.NewGuid()}" });
});

app.Run();

// ---------------------------------------------------------
// 4. Custom Exception Handler Implementation
// ---------------------------------------------------------
public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        // A. Logging the error (Crucial for debugging)
        _logger.LogError(exception, "An unhandled exception occurred.");

        // B. Determine the ProblemDetails instance based on exception type
        var problemDetails = CreateProblemDetails(httpContext, exception);

        // C. Write the standardized response
        httpContext.Response.StatusCode = problemDetails.Status ?? 500;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        // Return true to signal that the exception was handled
        return true;
    }

    private static ProblemDetails CreateProblemDetails(HttpContext httpContext, Exception exception)
    {
        // Map specific exceptions to HTTP status codes and AI-specific error types
        return exception switch
        {
            // Simulating an AI Content Filter violation
            UnauthorizedAccessException => new ProblemDetails
            {
                Title = "Access Denied",
                Detail = "The requested AI model is restricted.",
                Status = StatusCodes.Status403Forbidden,
                Type = "https://api.ai/errors/content-filter",
                Instance = httpContext.Request.Path,
                Extensions = { { "errorCode", "AI_403" } }
            },

            // Simulating a generic logic error (e.g., invalid input)
            InvalidOperationException => new ProblemDetails
            {
                Title = "Invalid Operation",
                Detail = exception.Message,
                Status = StatusCodes.Status400BadRequest,
                Type = "https://api.ai/errors/invalid-input",
                Instance = httpContext.Request.Path,
                Extensions = { { "errorCode", "AI_400" } }
            },

            // Simulating a critical infrastructure failure
            OutOfMemoryException => new ProblemDetails
            {
                Title = "Service Unavailable",
                Detail = "The AI processing engine is currently overloaded.",
                Status = StatusCodes.Status503ServiceUnavailable,
                Type = "https://api.ai/errors/service-overload",
                Instance = httpContext.Request.Path,
                Extensions = { { "retryAfter", 30 } } // Custom extension for AI retry logic
            },

            // Fallback for all other unhandled exceptions
            _ => new ProblemDetails
            {
                Title = "An unexpected error occurred",
                Detail = "An internal error prevented the request from completing.",
                Status = StatusCodes.Status500InternalServerError,
                Type = "https://api.ai/errors/internal",
                Instance = httpContext.Request.Path,
                Extensions = { { "traceId", Activity.Current?.Id ?? httpContext.TraceIdentifier } }
            }
        };
    }
}

Detailed Line-by-Line Explanation

1. Service Configuration

  • builder.Services.AddProblemDetails();: This line registers the ProblemDetails service. It configures the application to use the ProblemDetails object model (defined in RFC 7807) for error responses. This ensures that errors are returned as JSON with standardized fields like type, title, status, and detail, rather than raw exception strings or HTML.
  • builder.Services.AddExceptionHandler<GlobalExceptionHandler>();: This registers our custom handler class (GlobalExceptionHandler) with the dependency injection container. It tells ASP.NET Core that whenever an unhandled exception occurs, this specific class should be responsible for processing it.

2. Middleware Pipeline

  • app.UseExceptionHandler();: This is the middleware that actually wires up the exception handling logic to the request pipeline. It catches exceptions thrown during the execution of subsequent middleware or endpoints and delegates handling to the registered GlobalExceptionHandler. Without this, the AddExceptionHandler registration would have no effect.

3. The Endpoint

  • app.MapGet("/generate-image", ...): Defines a simple GET endpoint.
  • throw new InvalidOperationException(...): Simulates a business logic validation failure. In a real AI API, this might happen if the input prompt violates safety guidelines or formatting rules.
  • throw new OutOfMemoryException(...): Simulates a critical system-level failure. In AI workloads, this often happens when model inference runs out of VRAM (Video RAM). We want to return a specific 503 Service Unavailable status here so the client knows to retry later, rather than a generic 500.

4. The GlobalExceptionHandler Class

  • public sealed class GlobalExceptionHandler : IExceptionHandler: We implement the IExceptionHandler interface. The sealed keyword is a modern C# optimization indicating that this class cannot be inherited, allowing the JIT compiler to perform certain optimizations.
  • TryHandleAsync: This is the core method.
    • Parameters: It receives the HttpContext, the Exception object, and a CancellationToken.
    • Logging: We inject ILogger to log the error. This is vital for backend debugging; the client sees the ProblemDetails, but the developer sees the full stack trace in logs.
    • Pattern Matching (switch expression): We use a C# pattern match to inspect the exception type.
      • UnauthorizedAccessException: Maps to a 403 Forbidden. We add a custom errorCode to the Extensions dictionary. This is how you pass AI-specific metadata (like "ContentFiltered") to the client.
      • OutOfMemoryException: Maps to 503 Service Unavailable. We add a retryAfter extension, instructing the client when to try again.
      • _ (Discard): The catch-all. We use Activity.Current?.Id to grab the current distributed tracing ID. This allows the client to report this specific ID to support, linking their error view directly to our backend logs.
    • Response Writing: httpContext.Response.WriteAsJsonAsync serializes the ProblemDetails object directly to the response body with the correct application/problem+json content type (handled automatically by the framework when writing a ProblemDetails object).

Visualizing the Flow

The following diagram illustrates how an exception travels through the middleware pipeline before being caught and formatted by our handler.

This diagram illustrates the flow of an exception as it travels through the middleware pipeline, where it is ultimately caught and serialized into a structured ProblemDetails JSON response with the correct application/problem+json content type.
Hold "Ctrl" to enable pan & zoom

This diagram illustrates the flow of an exception as it travels through the middleware pipeline, where it is ultimately caught and serialized into a structured `ProblemDetails` JSON response with the correct `application/problem+json` content type.

Common Pitfalls

  1. Forgetting app.UseExceptionHandler(): A frequent mistake is registering the handler via AddExceptionHandler but omitting the middleware call app.UseExceptionHandler(). In this scenario, the exception will propagate up to the host server (Kestrel/IIS), which will likely terminate the connection abruptly or return a default HTML error page, completely bypassing your custom ProblemDetails logic.

  2. Swallowing the Exception: Ensure that TryHandleAsync returns true only if you have actually handled the response. If you return false, the exception will continue to propagate up the middleware chain. If no other handler catches it, the application will crash or fall back to the server's default error handling.

  3. Exposing Sensitive Data in Detail: When mapping the catch-all _ exception, avoid setting Detail = exception.Message. Exception messages often contain stack traces, file paths, or internal variable values. Always use a generic message for unknown errors (e.g., "An unexpected error occurred") and rely on the traceId to correlate the error with server logs.

  4. Ignoring Content Negotiation: While WriteAsJsonAsync forces JSON, in a production API, clients might request different formats. The ProblemDetails service in ASP.NET Core automatically handles content negotiation, but if you manually write to the response stream without using the built-in serialization helpers, you might fail to respect the Accept header. Always prefer WriteAsJsonAsync or the built-in Results.Problem() helper when possible.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon


Loading knowledge check...



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.