“早抛出、晚捕获”是异常处理的关键原则:异常应在问题源头(如DAO、SDK层)立即抛出,明确语义;捕获应推迟至能决策的高层(如网关、编排层),避免中间层假捕获导致语义失真与调试成本上升。

“早抛出、晚捕获”不是一句空话,而是异常处理在系统架构中落地的关键原则——它要求异常在发生点尽快抛出,不被静默吞掉或过早封装;而捕获则应推迟到有足够上下文、能做出合理决策的高层位置,比如业务编排层或网关层。
异常该在哪儿抛?越靠近问题源头越好
底层模块(如DAO、SDK调用、文件读写)一旦发现不可恢复的错误(如数据库连接失败、HTTP 500响应、JSON解析失败),应立即抛出明确异常,不尝试“兜底”或返回null/错误码。返回错误码容易导致上层忽略,而用异常能强制调用方关注。
- DAO层查不到数据,抛 NotFoundException,而不是返回 null 让 Service 层自己判空
- 调用第三方API超时,抛 RemoteCallTimeoutException,而不是吞掉异常并返回默认值
- 配置缺失或格式错误,在初始化阶段就抛 ConfigurationException,不等到运行时才暴露
异常该在哪儿捕?只在能处理或需要统一策略的地方
捕获异常的位置决定了系统的可观测性与可维护性。低层模块不该“自作主张”地 try-catch 并记录日志或重试——那会割裂调用链路、掩盖真实语义。真正适合捕获的位置通常是:
- API网关或Controller层:统一转换为标准错误响应(如400/500 + 错误码 + 用户友好提示)
- 业务流程编排层(如Saga事务协调器):根据异常类型决定补偿动作、重试策略或降级逻辑
- 任务调度入口(如Quartz Job):捕获后记录失败原因、触发告警、更新任务状态
避免中间层“假捕获”,防止异常语义失真
常见反模式是Service层看到DAO抛了异常,立刻用try-catch包一层再抛新异常,却不保留原始堆栈或关键信息。这会让问题定位变难,也模糊了责任边界。
- 不要把 SQLException 简单转成 RuntimeException 并丢掉cause
- 若需增强上下文(如加上订单ID),用 throw new BusinessException("下单失败", e),确保e作为cause传入
- 日志记录建议放在最终捕获点,而非每层都log.error——避免重复刷屏、干扰根因分析
配合架构分层,让异常流自然“浮上来”
良好的分层设计本身就能支撑早抛晚捕:基础设施层专注“出问题就报”,领域层专注“什么问题”,应用层专注“怎么应对”。比如支付服务中:
- 支付渠道SDK层:网络异常 → 抛 ChannelNetworkException
- 支付领域服务:余额不足 → 抛 InsufficientBalanceException
- 订单应用服务:捕获上述异常,决定是提示用户充值、切换支付方式,还是回滚创建订单事务
不复杂但容易忽略——早抛出是尊重事实,晚捕获是尊重职责。架构里每多一层无意义的catch,就多一分调试成本和语义损耗。

















