ASP.NET Core全局异常处理核心是UseExceptionHandler中间件,必须注册在UseRouting之后、UseEndpoints之前;它仅捕获HTTP请求上下文中的未处理异常,后台任务异常需手动try-catch并记录日志。

ASP.NET Core 里用 UseExceptionHandler 捕获所有未处理异常
全局异常处理在 ASP.NET Core 中不靠 AppDomain.UnhandledException(它在 .NET Core+ 已失效),而是走中间件管道。核心就是 UseExceptionHandler,它必须在 UseRouting 之后、UseEndpoints 之前注册。
常见错误是把它放在 UseStaticFiles 后面却没注意顺序,结果 404 或静态资源路径错误时异常根本进不来中间件;或者在 Startup.Configure 里写成 app.UseExceptionHandler("/error"),但没配对应路由,导致 500 页面空白或 404。
-
UseExceptionHandler默认只捕获 HTTP 请求上下文中的未处理异常,后台任务(如HostedService)抛出的异常不会经过它 - 传入的路径(如
"/error")必须对应一个实际存在的控制器 Action,且该 Action 不能自己再抛异常,否则会循环触发 - 若想记录原始异常堆栈,需从
HttpContext.Features.Get<iexceptionhandlerfeature>()</iexceptionhandlerfeature>提取Error属性,而不是依赖ModelState或参数绑定
自定义异常响应体:避免暴露敏感信息
直接返回 ProblemDetails 是安全做法,但默认行为可能泄露环境(比如开发环境下显示完整堆栈,生产环境却只返回空对象)。关键不是“要不要写 JSON”,而是控制什么字段能出去。
示例中常有人写 return new JsonResult(new { message = ex.Message }),这既绕过 ProblemDetails 标准,又容易把内部路径、数据库名等打到前端。正确方式是复用框架内置机制:
- 调用
context.Response.WriteAsJsonAsync(new ProblemDetails { Status = 500, Title = "服务器错误" }),手动构造时务必清空Detail和Instance - 开发环境启用
DeveloperExceptionPage(仅限env.IsDevelopment()),生产环境禁用所有堆栈输出 - 若用
UseExceptionHandler("/error"),确保/errorAction 返回的是ObjectResult而非ViewResult,否则 JSON API 客户端收不到结构化错误
后台任务和定时器里的异常怎么兜住
UseExceptionHandler 对 Task.Run、Timer、IHostedService.StartAsync 里的异常完全无效——这些线程不在 HTTP 上下文中,CLR 会直接终止线程,但进程未必崩溃,错误就静默丢失了。
必须显式包一层 try/catch,并配合日志记录。尤其注意 async void 方法(如 Timer 回调),它无法被外层 catch 捕获,必须在方法体内处理:
- 所有
async void方法(包括Timer.Elapsed的事件处理器)内部必须有try/catch,且至少记录ex.ToString() -
IHostedService的StartAsync和StopAsync方法里,任何 await 都要包在 try/catch 中,否则服务启动失败却不报错 - 不要依赖
TaskScheduler.UnobservedTaskException,.NET 6+ 默认已禁用该事件,且它只对“从未 await 也从未检查Exception属性”的 Task 生效
为什么 try/catch 里再 throw 有时会丢掉堆栈
写 catch { throw; } 是保留原始堆栈的关键,但很多人误写成 catch (Exception ex) { throw ex; }——后者会重置堆栈帧,导致日志里看到的异常位置永远是 throw ex 这一行,而不是真正出问题的地方。
这个细节在全局异常日志里特别致命:你看到的“发生位置”可能是某个通用包装方法,而不是业务代码第 87 行的 _repo.Update() 调用。
- 无参数
throw;重抛原异常,堆栈不变 -
throw ex;创建新异常实例,堆栈从这行开始重建 - 若需补充上下文(如请求 ID),用
throw new CustomException("msg", ex),把原异常作为InnerException传入
全局异常处理真正的复杂点不在中间件配置,而在于**异常发生的上下文是否受控**:HTTP 请求流、后台线程、异步回调、终结器线程……每个场景的兜底方式都不同,且不能互相替代。漏掉任意一类,错误就可能悄无声息地消失。











