查看: 100|回覆: 4

.NET 规范异常捕获 & 处理

[複製鏈接]

2

主題

1

回帖

0

積分

热心网友

金币
1
閲讀權限
220
精華
0
威望
0
贡献
0
在線時間
0 小時
註冊時間
2010-9-22
發表於 2026-4-29 09:17:00 | 顯示全部樓層 |閲讀模式

一、核心规则

 
  1. 异常仅用于非预期错误,禁止用来做业务逻辑判断(替代 if/TryXXX)。
  2. 精准捕获:抓具体异常,禁止无脑捕获 Exception
  3. 禁止空捕获 catch{}、吞异常、隐藏故障。
  4. 重抛异常只用裸 throw;,禁用 throw ex;(丢失堆栈)。
  5. 资源释放优先 using,少手写 finally
  6. 优先使用 when 异常过滤器,缩小捕获范围,代码更干净。
 

 

二、高频错误写法

 

1. 全量捕获

 
// 错误:所有错误全吃掉,线上Bug无法排查
try
{
    
}
catch (Exception ex)
{
    Log(ex);
}
 

2. 空捕获 / 静默失败

 
// 极端错误:完全吞掉异常,无日志无提示
try { }
catch
{

}
 

3. 截断异常堆栈

 
catch (SqlException ex)
{
    throw ex; // 错误:重置调用堆栈,排查直接报废
}
 

4. 异常替代常规判断

 
// 错误:用异常控制正常业务
try
{
    int id = int.Parse(input);
}
catch
{
    id = 0;
}

// 正确:预判优先
int.TryParse(input, out int id);
 

 

三、标准正确写法

 

1. 基础:精准捕获指定异常

 
try
{
    string text = File.ReadAllText("data.txt");
}
catch (FileNotFoundException ex)
{
    _logger.LogWarning(ex, "目标文件不存在");
    // 友好业务提示 / 降级处理
}
catch (IOException ex)
{
    _logger.LogError(ex, "文件读写失败");
}
 

2. 进阶:when 过滤器

 
在捕获前做条件过滤,不进入 Catch 代码块、不破坏堆栈,替代 Catch 内部 if
 
try
{
    await _dbContext.SaveChangesAsync();
}
// 仅捕获 SQL 唯一键冲突
catch (SqlException ex) when (ex.Number is 2627)
{
    throw new BusinessException("数据重复,请勿重复提交");
}
// 仅捕获外键约束错误
catch (SqlException ex) when (ex.Number is 547)
{
    throw new BusinessException("关联数据不存在,操作失败");
}
// 仅捕获数据库死锁
catch (SqlException ex) when (ex.Number is 1205)
{
    throw new BusinessException("系统繁忙,请稍后重试");
}
 

3. 保留原始堆栈重抛

 
catch (TimeoutException ex)
{
    _logger.LogError(ex, "服务调用超时");
    throw; // 正确:保留完整堆栈、调用链
}
 

4. 包装自定义业务异常(内层保留原异常)

 
catch (ArgumentNullException ex)
{
    // 第二个参数传入原异常,完整保留错误链路
    throw new BusinessException("必填参数不能为空", ex);
}
 

5. finally 资源兜底

 
FileStream? stream = null;
try
{
    stream = new FileStream(path, FileMode.Open);
}
catch (IOException ex)
{
    _logger.LogError(ex, "文件操作异常");
}
finally
{
    // 无论成败,必然释放
    stream?.Dispose();
}
 
优先替代方案:using var stream = new FileStream(path, FileMode.Open);
 

 

四、多层捕获 + 过滤

 
try
{
    // 数据库业务操作
}
catch (SqlException ex) when (ex.Number is 2627 or 547)
{
    // 针对性约束错误处理
}
catch (SqlException ex)
{
    // 其他数据库异常统一日志
    _logger.LogError(ex, "数据库执行异常");
    throw;
}
catch (OperationCanceledException)
{
    // 单独处理取消请求
}
 

 

五、ASP.NET Core 项目实践

 
  1. 业务层 / 仓储层:只捕获可恢复、可预知的特定异常 + when 过滤。
  2. Controller / 接口层:不手写大量 try-catch
  3. 全局统一异常中间件 / 过滤器兜底:
    • 记录完整异常 + 堆栈日志;
    • 屏蔽原始错误堆栈返回前端;
    • 输出标准化错误响应。
     
 

 

六、总结

 
  1. 抓具体,不抓 Exception
  2. 能用 when 过滤,不写 Catch 内部 If;
  3. 重抛用 throw,拒绝 throw ex
  4. 异常不吞、必留日志;
  5. 正常判断用 TryParse/If,不走异常;
  6. 资源一律 using,干净安全。


来源:https://www.cnblogs.com/chuansheng/p/19935530
回覆

使用道具 舉報

0

主題

67

回帖

84

積分

琼殿精英

金币
17
閲讀權限
220
精華
0
威望
0
贡献
0
在線時間
0 小時
註冊時間
2010-9-27
發表於 2026-5-6 11:06:44 | 顯示全部樓層
谢谢楼主总结,这些坑估计老手新手都踩过,尤其是“吞异常”和
  1. throw ex;
複製代碼
真的会让半夜排查的同事血压拉满 [s:ac:瞎]  
我补充一点自己的习惯:在配合
  1. using
複製代碼
的同时,如果非得自己写
  1. Dispose
複製代碼
逻辑,最好用
  1. try-finally
複製代碼
并且把
  1. Dispose
複製代碼
的异常也单独处理,不然资源释放里再抛异常会盖掉原始异常,排查更难。  

另外楼主提的
  1. when
複製代碼
过滤器确实香,比如:  
  1.   
  2. catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)  
  3. {  
  4.     // 只处理 404,其他状态码让上层处理  
  5. }  
複製代碼
  
这样比在
  1. catch
複製代碼
块里再
  1. if-else
複製代碼
清爽多了,而且不会错误地“捕获并重抛”丢堆栈。  

大家有没有遇到过那种祖传代码里
  1. catch (Exception) { //TODO }
複製代碼
的惊吓?说出来让大伙乐呵乐呵 [s:ac:哭笑]
回覆

使用道具 舉報

0

主題

11

回帖

77

積分

琼殿精英

金币
66
閲讀權限
220
精華
0
威望
0
贡献
0
在線時間
0 小時
註冊時間
2008-8-4
發表於 2026-5-6 15:40:24 | 顯示全部樓層
好帖,收藏了![s:2]

楼主这总结得相当到位,特别是 when 过滤器 这块,很多 .NET 老鸟都没形成习惯。我补充几点实战中踩过的坑:
关于第4点 "重抛异常只用裸 throw;"
这个必须顶,但想提醒一嘴:async/await 场景下 throw; 和 throw ex; 的堆栈行为在 .NET Framework 和 .NET Core 里表现不完全一样,.NET Core 之后做了优化,但老项目升级时要留个心眼。

另外楼主提到的全局异常中间件,我补个常见的 ExceptionFilter 模板:

[code=csharp]
public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        // 已经被处理过的不要重复介入
        if (context.ExceptionHandled) return;

        _logger.LogError(context.Exception, "全局捕获未处理异常");

        context.Result = new JsonResult(new
        {
            Code = 500,
            Message = "系统繁忙,请稍后重试",
            // 开发环境可以带 TraceId,生产环境绝对不要暴露 StackTrace
            TraceId = Activity.Current?.Id ?? context.HttpContext.TraceIdentifier
        });

        context.ExceptionHandled = true;
    }
}
[/code]

还有个小众但实用的:ExceptionDispatchInfo.Capture(ex).Throw();
适合需要在 await 之后重新抛异常但想保留原始上下文 的骚操作,配合 Task.WhenAll 用得上。

最后想问问楼主,Exception 的 InnerException 链 在微服务场景下你们怎么传递?我们目前是在网关层统一收敛,但链路追踪时偶尔会有断层,有没有更好的实践?[s:5]
回覆

使用道具 舉報

0

主題

66

回帖

71

積分

琼殿精英

金币
5
閲讀權限
220
精華
0
威望
0
贡献
0
在線時間
0 小時
註冊時間
2012-4-30
發表於 2026-5-6 15:40:34 | 顯示全部樓層
楼主总结得太到位了! 这些规范真的是用血泪教训换来的,尤其是堆栈信息丢失的问题,之前项目里有人图省事写了
  1. throw ex;
複製代碼
,结果线上报错直接定位不到源头,排查的时候真是急得冒汗。

我平时还会多注意两点:一是尽量在应用层做全局异常拦截,配合请求追踪ID一起打日志,这样就算底层抛了异常也能顺着链路快速还原现场;二是业务异常和系统异常最好严格区分,业务异常走自定义状态码返回友好提示,系统异常才触发统一告警,这样监控大盘会干净很多,也不会把敏感堆栈暴露给前端。

另外现在.NET生态里配合结构化日志和异常过滤器,排查效率真的直线上升。大家还有没有遇到过什么奇葩的异常处理姿势或者好用的封装方案?欢迎继续交流,这种干货贴必须支持!顶!
回覆

使用道具 舉報

0

主題

0

回帖

98

積分

琼殿精英

金币
97
閲讀權限
220
精華
0
威望
0
贡献
0
在線時間
0 小時
註冊時間
2012-5-8
發表於 2026-5-6 17:45:03 | 顯示全部樓層
前排点赞,楼主的核心规则和楼上补充的点全是踩坑踩出来的干货啊!
之前我们团队也踩过类似的坑,有个新人写导出功能的时候怕报错崩服务,直接套了个catch(Exception)然后返回空文件,线上跑了一周运营才说导出的文件全是坏的,日志里啥痕迹都没有,排查到秃头才找到问题。
给大家再补充个小知识点,如果遇到需要把异常跨线程/异步上下文传递再重抛的场景,别直接throw ex,用
  1. ExceptionDispatchInfo.Capture(ex).Throw()
複製代碼
,能完整保留原始堆栈信息,查问题的时候巨好用。
还有自定义业务异常的时候记得要实现序列化构造函数,不然异常在跨应用域传递或者序列化打日志的时候容易出幺蛾子,微软现在也不推荐自定义异常继承ApplicationException了,直接继承Exception就行,省得踩老规范的坑。
哦对还有性能相关的,正常流程能走TryXXX就绝对别用异常做逻辑判断,我们之前压测的时候发现高频场景下抛异常的性能比走TryParse慢几十倍,改完QPS直接上去一大截。
现在我们团队把这些规范都加到CI的代码扫描规则里了,不符合的直接提PR不让过,省得后续线上出问题排查起来费劲儿😂
回覆

使用道具 舉報

您需要登錄後才可以回帖 登錄 | 立即注册

本版積分規則

相关侵权、举报、投诉及建议等,请发 E-mail:qiongdian@foxmail.com

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部