在开发.NET应用时,异常处理是保障程序健壮性的核心工作之一。局部异常可以通过try-catch块来处理,但总有一些异常会从业务代码中“逃逸”出来,比如第三方库抛出的未预期异常、后台线程中的错误、异步任务中未观察到的故障等。如果这些异常没有被捕获,轻则导致当前请求失败,重则让整个进程崩溃。全局异常处理机制就是为了给这类问题兜底,它允许开发者在应用边界统一拦截未处理异常,记录日志、清理资源、发送告警,甚至决定是否让应用继续运行。

不同.NET宿主环境提供了不同的全局异常处理入口。在控制台程序、Windows服务以及类库中,通常使用AppDomain.CurrentDomain.UnhandledException事件;在Windows Forms和WPF中,还有Application.ThreadException和DispatcherUnhandledException等专用事件;在ASP.NET Core中,则通过中间件或异常过滤器实现更灵活的处理。理解这些机制的区别和适用场景,是构建稳定应用的基础。下面将分别介绍这些方法,并给出可直接落地的代码示例。
基于AppDomain的全局异常捕获
AppDomain(应用程序域)是.NET中隔离程序集和资源的逻辑单元,每个.NET进程至少包含一个默认AppDomain。AppDomain.CurrentDomain.UnhandledException事件会在任何线程上出现未捕获异常时触发,无论是主线程还是后台线程,也不管异常发生在同步代码还是异步方法中(但异步任务有专门的事件,稍后介绍)。这个事件的主要作用是记录异常信息,并在必要时执行一些清理工作,但无法阻止进程随后终止——因为异常已经突破了所有catch块,运行时认为程序状态可能已损坏,默认策略是结束进程。
订阅该事件的典型做法是在程序入口点(如Main方法)中注册事件处理器。下面是一个控制台应用的示例,它展示了如何捕获未处理异常并写入日志文件:
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
// 注册全局异常处理器
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Exception ex = e.ExceptionObject as Exception;
string logPath = Path.Combine(AppContext.BaseDirectory, "unhandled.log");
File.AppendAllText(logPath, $"[{DateTime.Now}] 未处理异常:{ex?.ToString()}\r\n");
// 这里可以执行资源清理、发送告警邮件等
};
// 模拟一个未捕获的异常
throw new InvalidOperationException("测试全局异常");
}
}
上面的代码在异常抛出前注册了事件,当异常发生后,运行时触发UnhandledException,处理器将异常信息追加到日志文件中。需要注意的是,UnhandledException事件处理器中不应尝试恢复执行或长时间阻塞,因为此时进程即将退出。如果希望在某些情况下阻止进程终止,可以使用Environment.FailFast的替代方案,但通常不建议这么做,因为异常后的程序状态可能不可靠。
对于后台任务的未观察异常,.NET 4.0引入了TaskScheduler.UnobservedTaskException事件。当Task对象被垃圾回收时,如果它的异常没有被访问(例如通过Wait、Result或await),就会触发该事件。默认情况下,.NET 4.5及以后版本中未观察的异常不会导致进程崩溃,而是被静默忽略,但这会掩盖潜在的问题。因此建议订阅该事件并记录日志,示例代码如下:
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine($"未观察任务异常:{e.Exception}");
e.SetObserved(); // 标记为已观察,避免进程因该异常退出
};
// 启动一个会抛出异常的任务,但不等待也不观察结果
Task.Run(() => { throw new Exception("后台任务异常"); });
// 强制触发垃圾回收,使未观察异常被检测到
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine("主线程继续运行...");
Console.ReadKey();
}
}
调用e.SetObserved()可以将异常标记为已处理,防止其在将来影响进程。这个事件适合用来捕获任务中未处理的异常,但在实际生产环境中,更推荐在异步方法内部就使用try-catch处理,而不是依赖全局兜底,因为未观察异常往往意味着程序设计存在疏漏。
ASP.NET Core中的全局异常处理
ASP.NET Core应用通常承载Web API或MVC服务,未处理的异常会导致请求失败,并可能向客户端返回不友好的错误信息甚至堆栈细节。在ASP.NET Core中,可以有多种方式实现全局异常处理,其中最常用的是通过中间件管道和MVC异常过滤器。中间件方式可以覆盖整个管道,处理包括MVC之外的所有请求异常;异常过滤器则更贴近MVC框架,能够捕获Action执行过程中的异常。
使用中间件进行全局异常处理的思路是:在管道最前面放置一个自定义中间件,用try-catch包裹next()调用,从而捕获后续所有中间件和终结点产生的异常。下面是一个完整的中间件实现,它记录日志并返回统一的JSON错误响应:
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "请求处理过程中发生未处理异常");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = new
{
error = new
{
message = "服务器内部错误,请稍后重试",
detail = exception.Message // 生产环境建议隐藏详细信息
}
};
return context.Response.WriteAsJsonAsync(response);
}
}
在Program.cs中注册该中间件时,需要将其放在管道的最前面,以确保能够捕获后续所有组件抛出的异常:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.UseMiddleware<GlobalExceptionMiddleware>(); app.MapControllers(); app.Run();
除了中间件,MVC的异常过滤器(IExceptionFilter或IAsyncExceptionFilter)也常用来处理Action内的异常。过滤器的作用范围仅限于MVC管道,但它的优势是可以根据Controller或Action进行精细化控制。下面是自定义异常过滤器的示例:
public class CustomExceptionFilter : IExceptionFilter
{
private readonly ILogger<CustomExceptionFilter> _logger;
public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "MVC捕获到未处理异常");
context.Result = new ObjectResult(new
{
error = new
{
message = "发生错误,请稍后重试",
detail = context.Exception.Message
}
})
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true; // 标记为已处理
}
}
然后在Program.cs中通过MVC选项注册该过滤器:
builder.Services.AddControllers(options =>
{
options.Filters.Add<CustomExceptionFilter>();
});
中间件和过滤器各有适用场景。中间件覆盖范围更广,适合作为应用级的最终屏障;过滤器与MVC集成更紧密,可以方便地获取Controller和Action的上下文信息。在很多项目中,两者可以结合使用,中间件负责兜底,过滤器负责特定区域内的异常处理和自定义响应。
桌面应用与特殊宿主环境的处理
Windows Forms和WPF这类桌面应用有它们自己的异常处理机制。Windows Forms提供了Application.ThreadException事件,用于捕获UI线程上发生的未处理异常。默认情况下,如果UI线程抛出未捕获异常,Windows Forms会弹出一个错误对话框,但可能不会终止应用。通过订阅该事件,开发者可以自定义错误提示、记录日志,并决定是否继续运行。示例代码如下:
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.ThreadException += (sender, e) =>
{
MessageBox.Show($"发生未处理异常:{e.Exception.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
// 记录日志、发送报告等
};
// 同时订阅AppDomain级事件,捕获非UI线程异常
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Exception ex = e.ExceptionObject as Exception;
MessageBox.Show($"严重错误:{ex?.Message}", "致命错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
};
Application.Run(new MainForm());
}
WPF应用则使用Application.DispatcherUnhandledException事件来处理UI线程上的未处理异常。该事件同样允许标记e.Handled = true来阻止应用崩溃。以下是一个简单的WPF全局异常处理示例:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
DispatcherUnhandledException += (sender, args) =>
{
MessageBox.Show($"发生异常:{args.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
args.Handled = true; // 防止应用退出
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Exception ex = args.ExceptionObject as Exception;
MessageBox.Show($"严重错误:{ex?.Message}", "致命错误", MessageBoxButton.OK, MessageBoxImage.Error);
};
}
}
对于Windows服务、控制台后台任务等没有UI的宿主,只能依赖AppDomain.UnhandledException和TaskScheduler.UnobservedTaskException。需要注意的是,Windows服务通常运行在非交互式会话中,不能弹出消息框,必须将异常信息写入Windows事件日志或文件,以便运维人员排查。此外,对于长时间运行的服务,还应当实现自动重启机制,例如利用服务控制管理器(SCM)的恢复选项,或者在捕获致命异常后主动调用Environment.Exit并让服务管理器重启进程。
在实际项目中,全局异常处理不应该替代局部异常处理。良好的异常处理策略是分层进行的:在可能发生异常的业务代码处使用try-catch进行局部恢复或转换,在应用边界使用全局机制作为最后防线。同时,务必记录足够的上下文信息,包括异常堆栈、请求参数、用户标识、时间戳等,以方便事后分析。日志记录可以使用成熟的日志框架如Serilog、NLog或内置的ILogger,并配置持久化到文件、数据库或集中式日志平台。
最后要提醒的是,全局异常处理本身也不应该抛出异常。处理器中的代码要尽量健壮,避免在记录日志时又引发新的异常导致双重故障。另外,对于Web应用,全局异常处理返回给客户端的错误响应应当隐藏内部实现细节,防止敏感信息泄露。而对于桌面应用,可以向用户展示友好的提示,但需要同时保留完整的异常信息用于调试。