订单导入程序收到一行文本:商品数量是 abc,支付服务暂时超时,日志里又出现了一条空引用。它们都叫“失败”,却不该用同一种方式处理:输入格式错误是普通分支,远程超时可能重试,违反订单规则需要明确拒绝,而空引用通常意味着代码缺陷。
这一章用“导入并提交订单”作为贯穿案例。我们不追求把每个异常都抓住,而是学习三件更重要的事:如何表达失败、谁有能力处理、资源如何保证收尾。
本章路线:失败分类 → 异常传播 → 捕获与清理 → 抛出与筛选 → 资源生命周期 → 领域错误 → 异步异常 → 应用边界。

先不要急着写 try。遇到失败时,先问:它是否可预期?当前方法能否恢复?调用方是否需要按失败种类做决定?
异常不是“更高级的 if”。异常会改变控制流、携带堆栈,并可能跨过多个方法;因此它适合罕见且不能在原地继续的路径。反过来,若失败是 API 的常见结果,显式返回值通常更容易阅读与组合。
异常是一个对象,至少应关注 Message、StackTrace 与 InnerException。当深层方法抛出异常,当前方法会立刻停止剩余语句;运行时沿调用栈向上寻找第一个匹配的 catch。这个逐层离开的过程称为栈展开。
static void ImportOrder(string line) => ParseAndSubmit(line);
static void ParseAndSubmit(string line)
{
Order order = ParseOrder(line);
Submit(order);
}
static Order ParseOrder(string line)
{
throw new FormatException("订单行缺少数量字段。");
}若三层都没有处理器,异常会成为未处理异常;控制台程序通常因此终止。catch 的目标不是阻止终止本身,而是在确实能够恢复、转换或建立错误边界的位置采取行动。
try 圈出可能失败的操作;catch 只接住类型兼容且筛选条件成立的异常;finally 则负责无论成功、已捕获异常还是继续向外传播都必须执行的收尾。
try
{
Order order = LoadOrder(orderId);
Submit(order);
Console.WriteLine("提交成功");
}
catch (OrderRuleException ex)
{
Console.WriteLine($"订单被拒绝:{ex.Code}");
}
catch (IOException ex)
{
Console.
可能输出:
订单被拒绝:ORDER_EMPTY
结束本次处理多个 catch 必须从具体到一般排列。OrderRuleException、IOException 应出现在 Exception 之前;否则宽泛分支会遮住具体分支,编译器也会拒绝不可达的捕获块。catch (Exception) 只适合应用边界的兜底,不应成为随手吞掉未知缺陷的工具。
finally 中应放简短、可靠的清理动作。不要从 finally 返回,也不要让它抛出另一个异常去遮盖原始失败。

方法检测到契约被破坏时,可以主动抛出异常。优先选语义准确的标准类型,并用 nameof 保持参数名可重构。
static void SetRetryLimit(int retryLimit)
{
if (retryLimit is < 0 or > 5)
throw new ArgumentOutOfRangeException(
nameof(retryLimit), retryLimit, "重试次数必须在 0 到 5 之间。");
}捕获后如果只是补充日志再继续传播,应写 throw;。它保留原始抛出位置;throw ex; 会把当前行变成新的抛出位置,削弱堆栈诊断价值。
try
{
await payment.ChargeAsync(order, cancellationToken);
}
catch (TimeoutException ex) when (order.AllowRetry)
{
logger.LogWarning(ex, "支付超时,OrderId={OrderId}", order.Id);
throw;
}筛选器 when 在进入 catch 前判断,适合按错误码、状态或重试策略进一步分流。筛选条件应无副作用且不能抛异常。
跨抽象边界时可以包装异常,让上层看到本层语义,同时把原异常放进 InnerException:
catch (IOException ex)
{
throw new OrderImportException("订单文件无法读取。", ex);
}
托管内存最终由垃圾回收器处理,但文件句柄、套接字、数据库连接等稀缺资源不能等到“以后”。实现 IDisposable 的对象应进入 using 作用域。
using FileStream stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();using 声明会在当前作用域结束时按创建的逆序调用 Dispose,其保证近似于 try/finally。即使中途 return 或抛异常,清理仍会发生。
异步资源实现 IAsyncDisposable 时,用 await using 等待异步清理:
await using DbTransaction transaction =
await connection.BeginTransactionAsync(cancellationToken);
await SaveOrderAsync(order, transaction, cancellationToken);
await transaction.CommitAsync(cancellationToken);不要同时手写 Dispose 又放进 using。除非类型契约明确允许,否则重复释放会制造新的边界问题。

标准异常足以覆盖大多数技术失败。只有调用方确实需要区分某种领域失败,且异常是这条调用路径的合适协议时,才定义自定义异常。
public sealed class OrderRuleException : Exception
{
public OrderRuleException(string code, string message)
: base(message) => Code = code;
public string Code { get; }
}
static void EnsureSubmittable(Order order)
{
if (order.Items.Count
若“被业务规则拒绝”非常常见,返回 Result 可能更合适:
public sealed record Result<T>(T? Value, string? Error)
{
public bool IsSuccess => Error is null;
}
static Result<Order> ParseOrder(string quantityText)
{
if (!int.TryParse(quantityText, out
选择标准很简单:调用者若需要在正常控制流中频繁分支,Result 更直白;若方法无法兑现契约且调用者通常不能就地恢复,异常更自然。不要为了“统一”而把两者强行混成一种。
async 方法中的异常会存进返回的 Task,在调用方 await 时重新抛出。因此要在包含 await 的位置捕获:
try
{
await SubmitOrderAsync(order, cancellationToken);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("用户取消了提交。");
}不要用 async void 承载普通业务操作;除事件处理器外,它无法被调用方等待,异常也难以通过正常任务链观察。取消也不等于故障:当调用方要求取消时,通常应让 OperationCanceledException 沿调用链传播,边界将其映射为“已取消”,而不是记录成系统错误。
整数溢出是另一类容易静默发生的失败。对金额计数等需要安全边界的运算使用 checked:
try
{
int total = checked(unitCount * unitPrice);
}
catch (OverflowException)
{
Console.WriteLine("计算结果超出 Int32 范围。");
}一个健康的订单处理链可以这样分层:
TryParse 或校验结果拒绝无效文本。try
{
await service.SubmitAsync(request, cancellationToken);
return Results.Ok();
}
catch (OrderRuleException ex)
{
return Results.BadRequest(new { code = ex.Code });
}
catch (Exception ex)
{
string incidentId = Guid.NewGuid().ToString(
日志记录事实和上下文,不记录密码、令牌、完整身份证号等敏感数据。用户消息则要稳定、可行动,不直接暴露堆栈与内部路径。

请实现 ImportAndSubmitAsync(string quantityText, CancellationToken token):
int.TryParse 解析数量,失败时返回 INVALID_QUANTITY。OrderRuleException。await using 管理异步事务,成功才提交。CANCELLED,不要当系统故障记录。建议至少测试这些路径:合法订单、非法文本、空订单、首次超时后成功、持续超时、用户取消、提交前数据库故障。每个测试都应同时断言返回结果与资源是否释放。
Result,无法兑现契约再用异常。catch。throw;,跨层包装要保留 InnerException。using,异步资源用 await using。真正可靠的程序不是“从不失败”,而是每一种失败都有清晰的所有者、可预测的控制流和完整的诊断线索。