1. 第一次看到那段代码时,我被“一个try catch”吓到了
那天我在Review一个订单服务的新接口,代码从Controller一路看到Service,突然停住了。不是因为逻辑多复杂,而是那段try catch写得太干净了,干净到让人怀疑自己是不是一直在用脚写Java。
大多数团队的try catch长什么样?大概是这样的:
java复制try {
orderService.createOrder(orderDTO);
} catch (Exception e) {
log.info("创建订单失败");
return Result.error("创建订单失败");
}
如果运气不好,还能看到更让人血压升高的版本:
java复制try {
orderService.createOrder(orderDTO);
} catch (Exception e) {
e.printStackTrace();
}
而那位从头部电商公司跳槽过来的工程师写的是这样:
java复制public Result<Void> createOrder(CreateOrderDTO dto) {
if (dto == null || dto.getUserId() == null) {
throw new BizException(ErrorCode.PARAM_NOT_NULL);
}
try {
orderService.createOrder(builder.from(dto));
return Result.success();
} catch (BizException e) {
log.warn("[order] create failed, biz error, bizNo={}, msg={}", dto.getBizNo(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
} catch (RetryableException e) {
log.error("[order] create failed, retryable error, bizNo={}", dto.getBizNo(), e);
return Result.error(ErrorCode.ORDER_CREATE_CONFLICT);
} catch (Exception e) {
log.error("[order] create failed, unknown error, bizNo={}", dto.getBizNo(), e);
return Result.error(ErrorCode.SYSTEM_ERROR);
} finally {
metric.record("order.create", System.currentTimeMillis() - start);
}
}
看到这段代码的一瞬间,我的第一反应是:这也太夸张了吧,写个try catch而已,有必要这么讲究吗?但等我冷静下来,把这段代码拆开看了一遍,才发现自己缺的不是语法知识,而是一整套关于异常处理的思考方式。
这篇文章我就想把这套思考方式彻底聊透。不管你现在用的是Java、Go还是JavaScript,只要写过判空、试错、兜底,就一定用得上看完这篇之后的几个判断准则。我会从最基础的try catch / finally / return执行机制讲起,一直讲到异常体系怎么设计、跟全局异常处理怎么配合、以及我在实际项目里踩过的那些“看起来正常但后患无穷”的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 优雅的底层:try catch / finally / return 的完整执行链路
如果想写出优雅的try catch,第一步不是学设计模式,而是真正搞懂try、catch、finally、return四者之间的执行顺序。很多同事写了好几年代码,在这上面都有错误认知。最典型的一个问题:try里先return了一个值,finally里又修改了这个值,最后方法返回的是哪个?
2.1 finally 和 return 到底谁先执行
直接说结论:在return表达式执行完之后、方法真正返回之前,会先执行finally块。用一段代码来验证再清楚不过:
java复制public static String testFinally() {
String result = "before";
try {
result = "in-try";
return result;
} finally {
result = "in-finally";
System.out.println("finally executed");
}
}
public static void main(String[] args) {
System.out.println(testFinally());
}
这段代码输出什么?答案是:
code复制finally executed
in-try
为什么?因为return result其实分两步走:先把result当前的值(也就是"in-try")存到临时槽里,然后执行finally,最后才把临时槽里的值返出去。finally里修改result,改的是变量本身,不是那个已经拷贝出来的临时值。
但是,这里有一个让很多人翻车的前提:这个临时值针对的是基本类型和String这种不可变对象。如果return的是
