阿杰,我来给你系统梳理一下 ASP.NET WebForms 全局异常捕获与处理的最佳实践,不只是代码,还包括架构思路和运维建议,让你在生产环境下既能完整记录异常,又能优雅提示用户。


1. 核心目标

全局异常处理主要解决三个问题:

  1. 拦截所有未处理异常(页面、控件、后台任务等)
  2. 记录日志(方便运维和问题排查)
  3. 友好提示用户(避免“黄屏死机”)

2. ASP.NET WebForms 全局异常处理入口

2.1 Application_Error

Global.asax 中定义:

void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();
    
    // 记录日志
    LogException(ex);

    // 清除异常,避免默认错误页
    Server.ClearError();

    // 跳转到友好错误页
    Response.Redirect("~/ErrorPage.aspx");
}

特点:

  • 可以捕获绝大多数运行时异常(页面生命周期、控件事件等)
  • 在请求处理流程的末端执行(HttpApplication 级别)
  • 如果异常在 HttpModule 级别发生,也能捕获

2.2 Page_Error

在单个页面捕获异常:

protected void Page_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();
    LogException(ex);
    Server.ClearError();
    Response.Redirect("~/ErrorPage.aspx");
}

适合场景:

  • 某个页面的异常希望自定义处理
  • 对全局影响不大

2.3 自定义 HttpModule

如果你想让异常处理逻辑模块化,可写一个模块:

public class ErrorHandlingModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.Error += new EventHandler(Context_Error);
    }

    private void Context_Error(object sender, EventArgs e)
    {
        var ex = ((HttpApplication)sender).Server.GetLastError();
        LogException(ex);
    }

    public void Dispose() { }
}

然后在 web.config 中注册:

<system.webServer>
  <modules>
    <add name="ErrorHandlingModule" type="Namespace.ErrorHandlingModule"/>
  </modules>
</system.webServer>

3. 日志记录最佳实践

3.1 使用成熟的日志库

示例(log4net):

private static readonly ILog log = LogManager.GetLogger(typeof(Global));

private void LogException(Exception ex)
{
    log.Error("未处理异常", ex);
}

3.2 记录的关键信息

  • 异常类型、消息、堆栈
  • 当前 URL、查询参数
  • 当前用户(如果已登录)
  • 客户端 IP、User-Agent
  • 发生时间

4. 用户友好提示页

4.1 自定义错误页

web.config 中配置:

<customErrors mode="On" defaultRedirect="ErrorPage.aspx">
  <error statusCode="404" redirect="NotFound.aspx"/>
</customErrors>
  • mode="On":始终显示自定义错误页
  • mode="RemoteOnly":本地显示详细错误,远程显示错误页

4.2 防止信息泄露

  • 不要在错误页显示堆栈信息
  • 可提供错误编号,让用户反馈时可在日志中查找

示例:

// ErrorPage.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
    string errorId = Guid.NewGuid().ToString();
    lblErrorId.Text = errorId;

    // 存到日志
    LogErrorId(errorId, Context.Items["LastError"] as Exception);
}

5. 进阶建议

  1. 分级处理异常
    • 业务可恢复异常:局部处理(try-catch)
    • 致命异常:全局捕获并记录
  2. 监控与告警
    • 配合邮件、短信、钉钉/企业微信机器人实时报警
  3. 区分开发和生产
    • 开发环境:详细异常(customErrors mode="Off"
    • 生产环境:友好提示 + 日志

6. 一个完整的全局处理方案示例

Global.asax

void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError().GetBaseException();

    // 生成错误 ID
    string errorId = Guid.NewGuid().ToString();

    // 记录日志
    LogManager.GetLogger("GlobalError").Error($"ErrorId: {errorId}", ex);

    // 清理异常
    Server.ClearError();

    // 存储错误 ID(可用于错误页显示)
    Context.Items["ErrorId"] = errorId;

    // 跳转错误页
    Response.Redirect("~/ErrorPage.aspx?eid=" + errorId);
}

ErrorPage.aspx

protected void Page_Load(object sender, EventArgs e)
{
    string eid = Request.QueryString["eid"];
    lblMessage.Text = $"系统发生错误,请联系管理员,并提供错误编号:{eid}";
}