try-catch谁都会写,但90%的程序员写的异常处理都有问题!今天分享异常处理的最佳实践,让你的代码更健壮、更容易调试!
一、异常处理的三大误区
误区1:try-catch包一切
// ❌ 错误:把所有代码都包起来
try
{
// 100行代码...
// 出错了根本不知道是哪行
}
catch(Exception ex)
{
// 处理异常
}
误区2:catch了啥也不干
// ❌ 错误:捕获异常却什么都没做
try
{
// 业务代码
}
catch(Exception ex)
{
// 空的!异常被吞了,调试时想哭
}
误区3:用异常控制流程
// ❌ 错误:用异常判断逻辑
try
{
int.Parse(userInput);
}
catch
{
// 输入不是数字
}
// ✅ 正确:用TryParse
if(int.TryParse(userInput,outint result))
{
// 是数字
}
else
{
// 不是数字
}
二、异常处理的核心原则
原则1:只捕获能处理的异常
// ✅ 正确:知道怎么处理才捕获
try
{
var file = File.OpenRead("config.json");
}
catch(FileNotFoundException ex)
{
// 文件不存在,用默认配置
UseDefaultConfig();
}
catch(UnauthorizedAccessException ex)
{
// 没权限,提示用户
ShowError("没有权限读取配置文件");
}
// 其他异常让上层处理
原则2:保留原始异常信息
// ❌ 错误:丢失原始异常
try
{
// 业务代码
}
catch(Exception ex)
{
throw new BusinessException("业务处理失败");
}
// ✅ 正确:保留原始异常
try
{
// 业务代码
}
catch(Exception ex)
{
throw new BusinessException("业务处理失败", ex);
}
原则3:具体异常优先
// ✅ 正确:先具体后通用
try
{
// 数据库操作
}
catch(SqlException ex)
// 1. 具体的SQL异常
{
LogSqlError(ex);
}
catch(DbException ex)
// 2. 数据库异常
{
LogDbError(ex);
}
catch(Exception ex)
// 3. 其他异常
{
LogError(ex);
throw;
// 处理不了就往上抛
}
三、finally的正确用法
释放资源的好地方
// 传统写法
FileStream fs =null;
try
{
fs = File.OpenRead("data.txt");
// 读取数据
}
finally
{
fs?.Close();
// 无论如何都会执行
}
// 现代写法(using语句)
using(var fs = File.OpenRead("data.txt"))
{
// 读取数据
}
// 自动释放
// 更现代写法
using var fs = File.OpenRead("data.txt");
// 读取数据
四、自定义异常:什么时候需要?
自定义异常的规则
// 需要自定义异常的场景:
// 1. 有特定的业务含义
// 2. 需要额外的属性
// 3. 需要特殊处理
public class OrderException:Exception
{
public string OrderId {get;}
public int ErrorCode {get;}
public OrderException(string orderId,string message):base(message){OrderId = orderId;}
public OrderException(string orderId,string message,Exception inner):base(message, inner){OrderId = orderId;}
}
// 使用
if(order.Total <0){throw new OrderException(order.Id,"订单金额不能为负数");}
什么时候不需要自定义?
五、全局异常处理
Web API全局处理
// .NET Core Web API 全局异常处理
public class GlobalExceptionFilter:IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger){_logger = logger;}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception,"请求处理异常");
var result =new{
Success =false,
Message ="系统开小差了,请稍后重试",
TraceId = Activity.Current?.Id ?? context.HttpContext.TraceIdentifier
};
context.Result =new JsonResult(result){StatusCode =500};
context.ExceptionHandled =true;}
}
// 注册
services.AddControllers(options =>{options.Filters.Add<GlobalExceptionFilter>();});
控制台/服务全局处理
// 全局未处理异常捕获
AppDomain.CurrentDomain.UnhandledException +=(sender, e)=>{
var ex = e.ExceptionObject asException;
Log.Fatal(ex,"发生未处理异常");
// 发送告警
AlertService.SendAlert($"程序崩溃:{ex?.Message}");
// 等待日志写入
Thread.Sleep(3000);
};
Task Scheduler.UnobservedTaskException +=(sender, e)=>{
Log.Error(e.Exception,"Task未观察异常");
e.SetObserved();
// 标记为已处理
};
六、日志记录的最佳实践
结构化日志(推荐Serilog)
// 配置
Log.Logger =new LoggerConfiguration().WriteTo.Console().WriteTo.File("logs/log-.txt",rollingInterval: RollingInterval.Day).WriteTo.ApplicationInsights(telemetryClient, TelemetryConverter.Traces).CreateLogger();
// 使用
try
{
// 业务代码
}
catch(Exception ex)
{
Log.Error(ex,"处理订单失败,订单ID:{OrderId}", orderId);
// 结构化日志会记录OrderId字段,方便查询
}
日志级别怎么选?
七、异常处理的6个实战技巧
技巧1:Try-Pattern避免异常
// ❌ 用异常
public User GetUser(int id){
try
{
return dbContext.Users.Find(id);
}
catch(Exception)
{
return null;
}
}
// ✅ Try-Pattern
public bool TryGetUser(int id,outUser user){
try
{
user = dbContext.Users.Find(id);
return user !=null;
}
catch
{
user =null;
return false;
}
}
// 使用
if(TryGetUser(1001,outvar user)){// 处理用户}
技巧2:异常过滤(Exception Filters)
// 只在特定条件下捕获异常
try
{
awaitCallExternalApi();
}
catch(HttpRequestException ex)
when(ex.StatusCode == HttpStatusCode.TooManyRequests)
{
// 限流了,等会重试
await Task.Delay(5000);
return await Retry();
}
catch(HttpRequestException ex)
when(ex.StatusCode >=500)
{
// 服务端错误,记录日志但不重试
Log.Error(ex,"外部服务异常");
throw;
}
技巧3:重试模式
public async Task<T>RetryAsync<T>(Func<Task<T>> action,int maxRetries =3){
int retryCount =0;
while(true){
try{return await action();}catch(Exception ex)when(IsTransient(ex)){retryCount++;if(retryCount >= maxRetries)throw;
// 指数退避
var delay = TimeSpan.FromSeconds(Math.Pow(2, retryCount));
await Task.Delay(delay);
}
}
}
private bool IsTransient(Exception ex){return ex isTimeoutException|| ex isSqlException sqlEx && sqlEx.Number ==-2|| ex isHttpRequestException httpEx &&(int)httpEx.StatusCode >=500;}技巧4:断路模式
public class CircuitBreaker{
private int _failCount;
private DateTime _openTime;
private readonly int _threshold =5;
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(30);
public async Task<T>ExecuteAsync<T>(Func<Task<T>> action){
if(_failCount >= _threshold){
if(DateTime.Now - _openTime < _timeout)throw new CircuitBreakerOpenException("服务暂时不可用");
// 半开状态,尝试恢复
_failCount =0;
}
try{
var result =await action();
_failCount =0;
return result;
}
catch
{_failCount++;
if(_failCount >= _threshold)_openTime = DateTime.Now;
throw;
}
}
}技巧5:Fallback降级
public async Task<User>GetUserAsync(int id){
try{
// 先查缓存
return await _cache.GetAsync<User>($"user:{id}");
}
catch(CacheException)
{
// 缓存挂了,直接查数据库
var user =await _db.GetUserAsync(id);
// 异步尝试恢复缓存(不等待)
_ = Task.Run(async()=>{
try{
await _cache.SetAsync($"user:{id}", user);
}
catch
{
// 缓存还是不行就算了
}
});
return user;
}
}技巧6:上下文传递
public class ExceptionWithContext:Exception{
public Dictionary<string,object> Context {get;}=new();
public ExceptionWithContext(string message):base(message){}
public ExceptionWithContext Add(string key,objectvalue){
Context[key]=value;
return this;
}
}
// 使用
throw new ExceptionWithContext("订单处理失败")
.Add("OrderId", orderId)
.Add("UserId", userId)
.Add("Total", order.Total);八、性能考量:异常真的慢吗?
异常的性能开销
// 测试结果(10万次操作)// 普通返回:0.5ms// 抛出异常:1200ms(慢了2000多倍!)// 所以:不要用异常做流程控制!
异常开销大的原因
收集调用堆栈
查找catch块
展开堆栈
执行finally
九、团队规范:异常处理检查清单
代码审查时问这几个问题:
这个异常真的能被处理吗?
捕获异常后做了什么?
原始异常信息保留了吗?
日志记录足够定位问题吗?
有没有用异常控制流程?
finally释放资源了吗?
自定义异常有必要吗?
十、记住这3句话
只捕获能处理的异常(处理不了就往上抛)
保留原始异常信息(别把堆栈弄丢了)
日志要能定位问题(不然等于没记)
该文章在 2026/2/26 11:01:27 编辑过