错误处理:我见过的三种糟糕写法

作者:晚风信箱 发布时间: 2026-08-04 阅读量:56 评论数:0

接手别人的代码时,我有个习惯:先全局搜一下捕获异常的关键字,看看有多少处、都是怎么写的。这个数字和写法,基本能预判我接下来半年的排查体验。

下面三种是我遇到最多的,每一种我都亲自被坑过。

第一种:捕获之后打条日志就完事

典型长这样:捕获所有异常,记一行日志,然后什么都不做,方法正常返回。

它的毒性在于把失败伪装成了成功。调用方拿到一个正常返回,继续往下走,用一个空的或者半初始化的对象做后续计算,最终在离出错点很远的地方炸掉,或者更糟——不炸,只是数据默默错了。

我们有一次对账不平,差了七百多笔。查到最后是一个同步任务里的这种写法:单条记录处理失败时打日志继续,日志级别还是 warn。那个日志一天打几万条,早就没人看了。整整两个月没人发现。

如果实在要吞,至少做到两点:一是记录足够定位的上下文,二是这个吞掉的行为要有计数指标,能在监控上看到它涨了。

第二种:全部转成统一异常,丢掉原因

这种写法通常出现在追求「统一错误处理」的项目里:每一层都捕获、包装成自己的业务异常、抛出去。看起来很规范。

问题出在包装的时候没把原始异常传下去,只取了一个消息字符串。于是最外层日志打出来是这样:业务处理失败。就这五个字,堆栈指向的是包装它的那一行,也就是异常处理代码本身,跟真正出错的位置隔了四层。

更离谱的变体是逐层包装,每层加一句自己的描述,最后消息变成一长串套娃,而真正有用的那句原始报错在最里面被截断了。

我的原则很简单:包装可以,但原因链必须完整传递。日志里必须能看到最初的那个堆栈。这一条我现在会在代码评审里坚持,因为它的收益是每次线上排查省二十分钟。

第三种:用返回值表达错误

方法失败了返回 null,或者返回 -1,或者返回 false。调用方需要知道这些约定,而这些约定往往只存在于写代码那个人的脑子里。

最难受的是无法区分「没有」和「出错了」。一个查询方法返回空,到底是数据确实不存在,还是下游超时了?这两种情况的处理方式完全相反:前者应该走默认逻辑,后者应该重试或者报错。返回一个空值,把这两件事糊成了一件。

我们线上出过一次事故,缓存查询在连接异常时返回空,调用方当成缓存未命中,回源打数据库。下游一抖,缓存全线返回空,几万请求瞬间全砸到数据库上,数据库被打挂。技术上叫缓存击穿,根因就是错误被表达成了「没有」。

顺便说几个小毛病

  • 日志里只打异常的消息,不打堆栈。这等于把最有价值的部分扔了。
  • 错误消息不带上下文。写「参数校验失败」,不写是哪个参数、传的什么值、期望是什么。我现在要求错误信息至少包含三要素:在做什么、什么输入、为什么不行
  • 捕获范围太大。一个 try 包了三十行,里面五个操作都可能失败,捕获之后没法区分是哪一个。
  • 在循环里捕获然后继续,但没有任何失败计数。跑完之后你不知道有没有失败、失败了几条。

我自己的两条判断

第一,处理一个错误之前先问:这个错误在这一层能不能被真正解决?能解决就在这里处理(比如重试、降级、走默认值),不能解决就原样往上抛,别在中途插手。中途插手却又解决不了,是绝大多数糟糕错误处理的来源。

第二,区分可预期的失败和不可预期的失败。用户输入不合法是可预期的,属于正常业务流程的一部分,用返回值表达完全合理;数据库连接断了是不可预期的,应该抛出去让上层决定。把这两类混在一起用同一套机制处理,代码一定会拧巴。

评论