async error handling
application_error not firing
ASP.NET error handling
web application debugging
asynchronous programming issues

Application_Error handler isn't fired when use prefix async

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In ASP.NET, the Application_Error handler in Global.asax does not catch exceptions thrown inside async methods because asynchronous exceptions propagate differently than synchronous ones. When an async method throws, the exception is captured into the Task object. If nothing awaits that task or the exception is not observed, it bypasses the ASP.NET pipeline error handling and Application_Error never fires. The fix is to ensure all async methods are properly awaited, use exception filters or middleware (in ASP.NET Core), or add TaskScheduler.UnobservedTaskException as a safety net.

The Problem

csharp
1// Global.asax.cs
2protected void Application_Error(object sender, EventArgs e)
3{
4    Exception ex = Server.GetLastError();
5    // Log the error
6    Logger.Error("Unhandled error", ex);
7    Server.ClearError();
8}
9
10// Controller — synchronous: Application_Error FIRES
11public ActionResult Index()
12{
13    throw new InvalidOperationException("Sync error");
14    // Application_Error catches this ✓
15}
16
17// Controller — async: Application_Error DOES NOT FIRE
18public async Task<ActionResult> IndexAsync()
19{
20    await Task.Delay(100);
21    throw new InvalidOperationException("Async error");
22    // Application_Error does NOT catch this ✗
23}

Why Async Exceptions Bypass Application_Error

In synchronous ASP.NET, exceptions propagate up the call stack through the ASP.NET pipeline, which triggers Application_Error. In async code, exceptions are stored in the Task object. The ASP.NET pipeline for MVC 4 and Web API does not always unwrap these task exceptions into the standard error pipeline, so Application_Error is never called.

csharp
1// What happens internally:
2// 1. Async method throws → exception stored in Task
3// 2. ASP.NET pipeline awaits the Task
4// 3. In some pipeline configurations, the exception is handled
5//    by the action invoker but NOT propagated to Application_Error

Fix 1: Use Exception Filters (ASP.NET MVC)

csharp
1// Create a global exception filter
2public class GlobalExceptionFilter : IExceptionFilter
3{
4    public void OnException(ExceptionContext filterContext)
5    {
6        Logger.Error("Unhandled error", filterContext.Exception);
7
8        filterContext.Result = new ViewResult
9        {
10            ViewName = "Error"
11        };
12        filterContext.ExceptionHandled = true;
13    }
14}
15
16// Register globally in FilterConfig.cs
17public class FilterConfig
18{
19    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
20    {
21        filters.Add(new GlobalExceptionFilter());
22    }
23}

Exception filters work with both sync and async action methods in ASP.NET MVC.

Fix 2: Use ExceptionHandler (ASP.NET Web API)

csharp
1// For Web API controllers
2public class GlobalExceptionHandler : ExceptionHandler
3{
4    public override void Handle(ExceptionHandlerContext context)
5    {
6        Logger.Error("API error", context.Exception);
7
8        context.Result = new TextPlainErrorResult
9        {
10            Request = context.ExceptionContext.Request,
11            Content = "An error occurred. Please try again."
12        };
13    }
14}
15
16public class GlobalExceptionLogger : ExceptionLogger
17{
18    public override void Log(ExceptionLoggerContext context)
19    {
20        Logger.Error("API exception", context.Exception);
21    }
22}
23
24// Register in WebApiConfig.cs
25config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
26config.Services.Add(typeof(IExceptionLogger), new GlobalExceptionLogger());

ASP.NET Core does not use Global.asax. Use middleware instead:

csharp
1// Custom exception handling middleware
2public class ExceptionMiddleware
3{
4    private readonly RequestDelegate _next;
5    private readonly ILogger<ExceptionMiddleware> _logger;
6
7    public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
8    {
9        _next = next;
10        _logger = logger;
11    }
12
13    public async Task InvokeAsync(HttpContext context)
14    {
15        try
16        {
17            await _next(context);  // Properly awaits — catches async exceptions
18        }
19        catch (Exception ex)
20        {
21            _logger.LogError(ex, "Unhandled exception");
22            context.Response.StatusCode = 500;
23            await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
24        }
25    }
26}
27
28// Register in Program.cs
29app.UseMiddleware<ExceptionMiddleware>();
30
31// Or use the built-in exception handler
32app.UseExceptionHandler("/Error");

Fix 4: Catch Unobserved Task Exceptions

As a safety net for fire-and-forget tasks:

csharp
1// In Application_Start or Program.cs
2TaskScheduler.UnobservedTaskException += (sender, args) =>
3{
4    Logger.Error("Unobserved task exception", args.Exception);
5    args.SetObserved();  // Prevent process termination
6};

Fix 5: Wrap Async Calls Properly

Ensure all async paths are awaited:

csharp
1// WRONG — fire-and-forget loses the exception
2public ActionResult TriggerJob()
3{
4    ProcessAsync();  // Not awaited — exception is lost
5    return View();
6}
7
8// CORRECT — await catches the exception
9public async Task<ActionResult> TriggerJob()
10{
11    await ProcessAsync();  // Exception propagates properly
12    return View();
13}
14
15// If you must fire-and-forget, catch inside the method
16public ActionResult TriggerJob()
17{
18    Task.Run(async () =>
19    {
20        try
21        {
22            await ProcessAsync();
23        }
24        catch (Exception ex)
25        {
26            Logger.Error("Background task failed", ex);
27        }
28    });
29    return View();
30}

ASP.NET Core Exception Handling Best Practice

csharp
1// Program.cs — complete setup
2var builder = WebApplication.CreateBuilder(args);
3var app = builder.Build();
4
5if (app.Environment.IsDevelopment())
6{
7    app.UseDeveloperExceptionPage();  // Detailed error page
8}
9else
10{
11    app.UseExceptionHandler("/Error");  // Production error page
12}
13
14// Or with ProblemDetails for APIs
15builder.Services.AddProblemDetails();
16app.UseExceptionHandler();
17app.UseStatusCodePages();

Common Pitfalls

  • Relying solely on Application_Error for async error handling: Application_Error in Global.asax was designed for the synchronous ASP.NET pipeline. It does not reliably catch exceptions from async controller actions. Use exception filters (MVC), ExceptionHandler (Web API), or middleware (ASP.NET Core) instead.
  • Fire-and-forget async calls without error handling: Calling an async method without await means its exception is never observed. The exception is silently swallowed (or triggers UnobservedTaskException later). Always await async calls, or wrap fire-and-forget calls in try-catch.
  • Using async void instead of async Task: async void methods cannot be awaited, and their exceptions crash the process instead of being caught by the pipeline. Only use async void for event handlers. Controller actions should always return Task<ActionResult>.
  • Not registering exception filters globally: Exception filters registered only on specific controllers miss errors in other controllers. Register GlobalExceptionFilter in FilterConfig (MVC) or WebApiConfig (Web API) to catch all unhandled exceptions.
  • Mixing ASP.NET Framework and Core patterns: Global.asax, Application_Error, and HandleErrorAttribute are ASP.NET Framework concepts. ASP.NET Core uses UseExceptionHandler() middleware and IExceptionFilter. Migrating to Core requires replacing the entire error handling strategy, not just adding async.

Summary

  • Application_Error does not catch async exceptions because they are captured in Task objects, not propagated through the ASP.NET pipeline
  • Use IExceptionFilter (MVC) or ExceptionHandler/ExceptionLogger (Web API) for ASP.NET Framework
  • Use UseExceptionHandler() middleware in ASP.NET Core — it properly awaits and catches async exceptions
  • Always await async methods — fire-and-forget calls silently swallow exceptions
  • Use TaskScheduler.UnobservedTaskException as a last-resort safety net for unobserved task exceptions

Course illustration
Course illustration

All Rights Reserved.