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:
- 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. - title: A short, human-readable summary of the problem.
- status: The HTTP status code (e.g., 429 for Too Many Requests).
- detail: A specific explanation for this occurrence (e.g., "Your request exceeded the token limit for model 'gpt-4-turbo'").
- instance: A URI reference to the specific request that caused the error.
- extensions: Arbitrary additional properties specific to the domain (e.g.,
retryAfterSecondsfor 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.
- The Request (The Item): A client sends an HTTP request. This item enters the facility (the API).
- 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.
- The Controller (The Packaging Station): The request reaches the controller action. This is where the AI model is invoked. The item is being processed.
- 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. - 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 Requestwith 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.
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.
- 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
IExceptionHandlerthat detects this specific signal and translates it into a Problem Detail with atypeofhttps://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. - 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 aretryAfterfield. The client can use this to disable the "Send" button for a specific duration. - 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
IExceptionHandlercan 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, ortype: "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
500error might be transient, but a400with typecontent-violationis 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:
- Catches exceptions anywhere in the pipeline.
- Classifies them based on domain logic (AI specific vs. generic).
- 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 theProblemDetailsservice. It configures the application to use theProblemDetailsobject model (defined in RFC 7807) for error responses. This ensures that errors are returned as JSON with standardized fields liketype,title,status, anddetail, 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 registeredGlobalExceptionHandler. Without this, theAddExceptionHandlerregistration 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 specific503 Service Unavailablestatus here so the client knows to retry later, rather than a generic500.
4. The GlobalExceptionHandler Class
public sealed class GlobalExceptionHandler : IExceptionHandler: We implement theIExceptionHandlerinterface. Thesealedkeyword 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, theExceptionobject, and aCancellationToken. - Logging: We inject
ILoggerto log the error. This is vital for backend debugging; the client sees theProblemDetails, but the developer sees the full stack trace in logs. - Pattern Matching (
switchexpression): We use a C# pattern match to inspect the exception type.UnauthorizedAccessException: Maps to a 403 Forbidden. We add a customerrorCodeto theExtensionsdictionary. This is how you pass AI-specific metadata (like "ContentFiltered") to the client.OutOfMemoryException: Maps to 503 Service Unavailable. We add aretryAfterextension, instructing the client when to try again._(Discard): The catch-all. We useActivity.Current?.Idto 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.WriteAsJsonAsyncserializes theProblemDetailsobject directly to the response body with the correctapplication/problem+jsoncontent type (handled automatically by the framework when writing aProblemDetailsobject).
- Parameters: It receives the
Visualizing the Flow
The following diagram illustrates how an exception travels through the middleware pipeline before being caught and formatted by our handler.
Common Pitfalls
-
Forgetting
app.UseExceptionHandler(): A frequent mistake is registering the handler viaAddExceptionHandlerbut omitting the middleware callapp.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 customProblemDetailslogic. -
Swallowing the Exception: Ensure that
TryHandleAsyncreturnstrueonly if you have actually handled the response. If you returnfalse, 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. -
Exposing Sensitive Data in
Detail: When mapping the catch-all_exception, avoid settingDetail = 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 thetraceIdto correlate the error with server logs. -
Ignoring Content Negotiation: While
WriteAsJsonAsyncforces JSON, in a production API, clients might request different formats. TheProblemDetailsservice 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 theAcceptheader. Always preferWriteAsJsonAsyncor the built-inResults.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.