Middleware provides a natural place to implement centralized error handling for agent interactions. Instead of scattering try-catch blocks across every call site, a single exception handling middleware wraps the entire agent pipeline and converts any exception into a graceful AgentResponse fallback.
Why Centralized Exception Handling?
- Graceful degradation — Users receive a friendly message instead of an unhandled exception crashing the host.
- Observability — Log or telemetry calls are written in one place, not duplicated everywhere.
- Typed recovery — Different exception types can produce different user-facing messages (timeout hint vs. general error).
The Exception Handling Pattern
async Task<AgentResponse> ExceptionHandlingMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
try
{
return await innerAgent.RunAsync(messages, session, options, cancellationToken);
}
catch (TimeoutException ex)
{
Console.WriteLine($"[ExceptionHandler] Caught timeout: {ex.Message}");
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
"Sorry, the request timed out. Please try again later.")
]);
}
catch (Exception ex)
{
Console.WriteLine($"[ExceptionHandler] Caught error: {ex.Message}");
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
"An error occurred while processing your request.")
]);
}
}
The middleware calls innerAgent.RunAsync() inside a try block. If an exception propagates from any deeper layer, the appropriate catch branch fires and returns a synthetic AgentResponse built from a plain ChatMessage.
Registering the Middleware
AIAgent safeAgent = baseAgent
.AsBuilder()
.Use(runFunc: ExceptionHandlingMiddleware, runStreamingFunc: null)
.Build();
Because the exception handler should be the outermost layer — the first thing that runs and the last thing that can catch — register it as the last .Use() call. In this framework the last .Use() wraps outermost.
Demo: Stacking with Fault Injection
To demonstrate all three paths without a real failing service, the demo in Lesson8/Demo1_ExceptionHandling.cs uses fault-injection middlewares as inner layers:
// TimeoutException path:
AIAgent timeoutAgent = baseAgent
.AsBuilder()
.Use(runFunc: TimeoutFaultMiddleware, runStreamingFunc: null) // inner - throws
.Use(runFunc: ExceptionHandlingMiddleware, runStreamingFunc: null) // outer - catches
.Build();
// General Exception path:
AIAgent crashAgent = baseAgent
.AsBuilder()
.Use(runFunc: CrashFaultMiddleware, runStreamingFunc: null) // inner - throws
.Use(runFunc: ExceptionHandlingMiddleware, runStreamingFunc: null) // outer - catches
.Build();
When timeoutAgent.RunAsync() is called, execution flows outward → inward:
ExceptionHandlingMiddleware enters its try block.- It calls
innerAgent.RunAsync() which is TimeoutFaultMiddleware. TimeoutFaultMiddleware throws TimeoutException.ExceptionHandlingMiddleware catches it and returns the fallback reply.
Middleware Execution Order
| Registration order (.Use) | Position in pipeline | Runs when |
|---|
First .Use() | Innermost | Last to execute (just before baseAgent) |
Last .Use() | Outermost | First to execute (can catch everything inside) |
This is why the exception handler must be registered last — it wraps everything else, so it catches exceptions from all inner middleware and the agent itself.
Typed vs. General Catch Blocks
| Exception type | Suggested user message | Typical cause |
|---|
TimeoutException | "The request timed out. Please try again." | LLM or downstream service slow to respond |
InvalidOperationException | "The operation is not valid. Please check your input." | Misuse of API, bad state |
Exception (catch-all) | "An error occurred while processing your request." | Any unexpected failure |
Always put the catch-all last so more specific handlers take precedence.
Key Types
| Type / Member | Package | Role |
|---|
AgentResponse(IList<ChatMessage>) | Microsoft.Agents.AI | Construct the fallback response returned by the catch block |
AIAgent.AsBuilder().Use().Build() | Microsoft.Agents.AI | Register middleware on an agent |
AgentRunMiddleware delegate | Microsoft.Agents.AI | Task<AgentResponse>(messages, session, options, innerAgent, ct) |
ChatMessage(ChatRole, string) | Microsoft.Extensions.AI | Create the fallback reply message |
Required Packages
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
Full Example
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
namespace MicrosoftAgentFrameworkLesson.ConsoleApp.Lesson8;
public static class Demo1_ExceptionHandling
{
public static async Task RunAsync(string apiKey)
{
Console.WriteLine("=== Demo: Exception Handling Middleware ===");
Console.WriteLine();
AIAgent baseAgent = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsAIAgent(
instructions: "You are a helpful assistant.",
name: "Assistant");
// Run 1: normal path - only the exception handler is registered.
AIAgent safeAgent = baseAgent
.AsBuilder()
.Use(runFunc: ExceptionHandlingMiddleware, runStreamingFunc: null)
.Build();
Console.WriteLine("User: What is the capital of France?");
AgentResponse r1 = await safeAgent.RunAsync("What is the capital of France?");
Console.WriteLine($"Agent: {r1.Text}");
Console.WriteLine();
// Run 2: TimeoutException path.
AIAgent timeoutAgent = baseAgent
.AsBuilder()
.Use(runFunc: TimeoutFaultMiddleware, runStreamingFunc: null)
.Use(runFunc: ExceptionHandlingMiddleware, runStreamingFunc: null)
.Build();
Console.WriteLine("User: [simulated timeout request]");
AgentResponse r2 = await timeoutAgent.RunAsync("Trigger a timeout");
Console.WriteLine($"Agent: {r2.Text}");
Console.WriteLine();
// Run 3: general Exception path.
AIAgent crashAgent = baseAgent
.AsBuilder()
.Use(runFunc: CrashFaultMiddleware, runStreamingFunc: null)
.Use(runFunc: ExceptionHandlingMiddleware, runStreamingFunc: null)
.Build();
Console.WriteLine("User: [simulated crash request]");
AgentResponse r3 = await crashAgent.RunAsync("Trigger a crash");
Console.WriteLine($"Agent: {r3.Text}");
Console.WriteLine();
}
static async Task<AgentResponse> ExceptionHandlingMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
try
{
Console.WriteLine(" [ExceptionHandler] Executing agent run...");
return await innerAgent
.RunAsync(messages, session, options, cancellationToken)
.ConfigureAwait(false);
}
catch (TimeoutException ex)
{
Console.WriteLine($" [ExceptionHandler] Caught timeout: {ex.Message}");
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
"Sorry, the request timed out. Please try again later.")
]);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($" [ExceptionHandler] Caught invalid operation: {ex.Message}");
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
"The operation is not valid in the current state. Please check your input.")
]);
}
catch (Exception ex)
{
Console.WriteLine($" [ExceptionHandler] Caught error: {ex.Message}");
return new AgentResponse(
[
new ChatMessage(ChatRole.Assistant,
"An error occurred while processing your request.")
]);
}
}
static Task<AgentResponse> TimeoutFaultMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
=> throw new TimeoutException("The operation exceeded the 30-second limit.");
static Task<AgentResponse> CrashFaultMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
=> throw new Exception("Unexpected downstream failure.");
}