上周五下午后端群里一个刚来半年的兄弟发了张截图问了一句“这个接口偶尔返回空数据但日志里啥报错都没有怎么排查”我看了一眼他贴出来的代码。问题一秒就找到了。// 就这玩意儿。看着人畜无害实际上是个完美犯罪现场asyncfunctiongetUserOrders(userId){try{constordersawaitorderService.findByUser(userId);returnorders;}catch(error){return[];// 把错误吞了还伪装成没有数据}}数据库连接超时了返回空数组。权限校验没过返回空数组。userId 根本不存在返回空数组。所有错误无论轻重缓急全部被这个catch一嘴吞掉了。然后假装一切正常给前端返回一个假的空列表。前端拿到空列表哦这个用户没有订单嘛。然后用户投诉我明明下了单怎么显示没有然后测试环境复现不了——因为测试环境数据库连接永远不超时。你的 try-catch 不是在处理错误。它在藏匿犯罪证据。这不是个别现象你随便翻一翻你手头的项目代码搜一下catch数数有多少个 catch 块里面只做了一件事console.log(error)然后返回一个默认值。或者更绝的——连console.log都没有。直接一个空的 catch 块一行代码不写。# Python 版本的完美犯罪。代码审查的时候还没人觉得有问题try:resultprocess_payment(order)exceptException:pass# 当事情没发生过一个pass。支付处理出了异常比如超时了、重复扣款了、第三方接口返回了非法响应——全部 pass。有个数据挺有说服力的大约20%的错误从来没有出现在日志里。它们被写代码的人处理掉了——这里的处理意思是捂住。我不确定写这种代码的人当时在想什么。可能是先跑起来再说可能是这里不太会出错的也可能纯粹就是嫌 IDE 底下那条红色波浪线看着烦。但不管动机是什么结果是一样的你把调试难度从看一眼日志就知道提升到了花三小时猜到底哪里出了问题。catch 了但没完全 catch比纯粹吞错误更阴间的是假装处理了。// 代码审查的时候能过关但上线之后出了事你根本查不到publicUserProfilegetUserProfile(LonguserId){try{returnuserRepository.findById(userId).orElseThrow(()-newUserNotFoundException(userId));}catch(Exceptione){log.error(获取用户信息失败);// 不打印堆栈、不记userId、不记异常类型returnnewUserProfile();// 返回一个空壳对象假装正常}}这段代码做了三件不亏心但全都没用的事第一catch (Exception e)捕获了所有异常——数据库挂了、网络超时了、空指针了全给兜住了。你永远不知道到底是哪种错。第二日志写了获取用户信息失败六个大字。好的然后呢是哪个用户什么原因失败堆栈呢——全没有。这条日志和没写一样。第三返回一个空的UserProfile。前端一看哦用户信息拿到了名字是null、手机号是null——然后页面上就一脸错愕地显示了一个信息全空的用户卡片。这代码跑在你本地的时候一切正常。但当它在生产环境里每天悄悄吞掉几十个异常的时候你唯一会注意到的信号就是客服群里越来越多的用户反馈信息不全工单。而你的日志里除了一大串没用的获取用户信息失败以外什么线索都没有。这不叫错误处理。这叫给未来的自己埋雷。那到底该怎么 catch说到底就两条原则。原则一你 catch 不了的别 catch。你的业务代码层不知道该怎么处理数据库连接超时——那你就别 catch 它。让它往上抛。让你的全局错误处理器来兜底让它统一记录、统一返回标准错误响应。// 不在业务层吞错误。让它爆出来asyncfunctiongetUserOrders(userId){// 出了异常直接抛出去。上游会有全局拦截器处理constordersawaitorderService.findByUser(userId);returnorders;}// 在应用入口统一兜底app.use((err,req,res,next){// 完整记录异常类型、消息、堆栈、请求上下文logger.error({message:err.message,stack:err.stack,url:req.originalUrl,userId:req.user?.id});res.status(500).json({error:服务异常请稍后重试});});业务层的代码反而变少了——你不需要每个函数都包一层 try-catch。你只需要在最外层兜一次底。原则二你要 catch 的话就给够信息。有些场景你确实需要在业务层做降级处理。比如调用第三方接口失败了你想用缓存数据兜底。当然可以。但你的 catch 块里至少要做三件事publicUserProfilegetUserProfile(LonguserId){try{returnuserRepository.findById(userId).orElseThrow(()-newUserNotFoundException(userId));}catch(UserNotFoundExceptione){// 1. 记清楚谁出了什么错log.warn(用户不存在, userId{},userId);throwe;// 2. 该爆还是爆——让调用方知道}catch(Exceptione){// 3. 意料之外的错误完整记录上下文log.error(获取用户信息时发生未知异常, userId{},userId,e);thrownewServiceException(用户服务暂时不可用,e);}}注意看跟前面那个假装处理版本的区别这里分开 catch 了不同的异常类型每条日志都带了 userId 和堆栈该抛出的异常老老实实抛出去了。不比那个catch (Exception e) { return new UserProfile(); }多了几行多了可能三四行。但就是这三四行决定了你凌晨两点被叫起来的时候是花5分钟在日志里精准定位问题还是花3小时在一堆获取用户信息失败的废话日志里大海捞针。回到开头那个后端群里的兄弟。我让他把那个catch (error) { return []; }改了。改成该抛的抛该记的记。他改完之后发现了一件更尴尬的事那个接口从上线第一天开始数据库查询就一直在间歇性超时——平均每天17次。只是之前所有超时全被那个return []给吞得干干净净。已经默默吃错误吃了整整三个月了。你也可以看看你项目里的 catch 块——尤其是那些catch (Exception e)后面只写了一行log.error的。它们在帮你处理问题还是在帮你隐瞒问题