接口返回ok:true但通知一封没发:空catch吞掉SQL报错的排查复盘

发布于 12天前 ❤ 0 👁 14 阅读 💬 2 评论

接口返回 ok:true,通知却一封没发:一个空 catch 吞掉了整条链路

背景:会展项目排期系统要给客户发"入住通知"。页面上点发送,接口返回 {"ok":true},后台日志干干净净,但酒店和客户一封都没收到。排查了三轮,最后在一行代码上找到原因。

先说结论:这不是邮件服务的问题,是一个 catch {} 把数据库报错吞了。

怎么查的。第一步不该猜邮件服务。我先确认副作用到底有没有发生:查数据库里有没有生成通知记录,看 SMTP/短信日志里有没有发出请求。两个都是空的——说明请求根本没走到发送那一步。接口返回成功、副作用为空,这个组合只说明一件事:中间某处的异常被吞掉了。

第二步,找吞异常的地方。整个项目搜 catch {}(注意是空 body,不带任何 console.error),改 bug 的时候搜这个比搜函数名有效得多。找到的位置在发通知前的数据准备逻辑里:代码先从主表查酒店的联系方式,再拼通知内容,整段包在 try/catch 里,catch 是空的。

第三步,临时把空 catch 改成打印错误再跑一次。日志立刻出来了:no such column: contact_phone。主表根本没有这一列,联系方式存在另一个字段里。SQL 报错 → 异常 → 被空 catch 吞掉 → 流程跳过整个发送环节 → 接口照常返回 ok。

坑在哪里。这个错误之所以难查,是因为它有三个特征叠加: 一是错误信息没有出口,不打印不写日志,谁也看不到; 二是它的失败是"完全静默的",接口的返回值跟成功时一模一样; 三是它失败的位置在发送之前,所以连邮件服务端的日志都是空的,很容易误以为服务没配好。

最迷惑的是第二点。正常设计里"发送失败"应该让接口返回失败,但真正坏掉的是失败之前的准备步骤,结果表现成"一切正常"。

怎么修的。先说修法,再说口径。 一,删掉空 catch。不追求 catch 到异常后还能优雅降级——这类准备逻辑出错就必须让请求失败,不能沉默。至少也要 console.error 打出来,绝不能留空。 二,修字段名。查清楚联系方式真实存在哪个字段,改正后再跑一遍。 三,补一层结果校验。接口的 ok 不应该只代表"流程没崩",而应该代表"副作用真的发生了"。发送环节记录成功/失败计数,任一失败就返回部分失败,让前端能看见。

可复用的方法,四条: 一,接口返回成功但副作用没发生时,别怀疑外部服务,先查"异常是否被吞掉"。最快的检查是全项目搜 catch {} 和 catch (e) {}。 二,判断副作用有没有发生,看数据库写入和下游日志,别信接口返回值。这是唯一可信的证据。 三,空 catch 在生产代码里没有合理场景。如果确实需要忽略异常,也要在注释里写清楚"为什么可以忽略",否则下一个人接手时,它就是下一个凌晨三点的工单。 四,查 SQL 报错最快的方式是让错误打出来。不要试图从代码里"看出"列名对不对,跑一次比读十遍有用。

这个问题从发现到修好不到二十分钟,但前面两轮都花在了错误的方向上——第一轮查 SMTP 配置,第二轮查客户邮箱。方向错了,时间全浪费。先确认"副作用发生到哪一步为止",比先怀疑哪个组件要省事得多。

评论 (2)

Meicheng · 站长
Meicheng · 站长 11天前
补一个能更早拦住这类问题的做法:把「空 catch」直接当 lint 错误处理。ESLint 的 no-empty、Python 的 bare-except 检查都能配成「catch 里必须至少打印或抛出」,上线前 CI 就能拦下来,不用等用户反馈才回查。另外 catch 里别只 console.error,顺手写一条结构化日志(时间+接口+原始报错),配合日志采集,事后翻起来比在控制台里捞快得多。
Meicheng · 站长
Meicheng · 站长 8天前
这个案例太典型了——「接口返回成功 + 副作用为空」的组合,几乎只可能是一处异常被吞了。补两条同源的经验:一是建议把「副作用可观测」做成默认习惯——发送前先写一条 pending 状态记录,发完再更新成 sent,这样接口说成功但状态表停在 pending,一眼就知道卡在发送前的哪一步,不用逐行排查。二是空 catch 之外,还有两个静默失败的常客:Promise.all 里某一个 reject 没单独接住(其他全成功,整体看似正常),以及 finally 里顺手把错误 return 掉。排查「成功但没生效」时,把这三处一起搜一遍。

登录 后参与评论