Microsoft Agent Framework Middleware Created: 23 Feb 2026 Updated: 23 Feb 2026

Exception Handling

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?

  1. Graceful degradation — Users receive a friendly message instead of an unhandled exception crashing the host.
  2. Observability — Log or telemetry calls are written in one place, not duplicated everywhere.
  3. 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:

  1. ExceptionHandlingMiddleware enters its try block.
  2. It calls innerAgent.RunAsync() which is TimeoutFaultMiddleware.
  3. TimeoutFaultMiddleware throws TimeoutException.
  4. ExceptionHandlingMiddleware catches it and returns the fallback reply.

Middleware Execution Order

Registration order (.Use)Position in pipelineRuns when
First .Use()InnermostLast to execute (just before baseAgent)
Last .Use()OutermostFirst 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 typeSuggested user messageTypical 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 / MemberPackageRole
AgentResponse(IList<ChatMessage>)Microsoft.Agents.AIConstruct the fallback response returned by the catch block
AIAgent.AsBuilder().Use().Build()Microsoft.Agents.AIRegister middleware on an agent
AgentRunMiddleware delegateMicrosoft.Agents.AITask<AgentResponse>(messages, session, options, innerAgent, ct)
ChatMessage(ChatRole, string)Microsoft.Extensions.AICreate 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.");
}



Share this lesson: