我已经受够了“系统异常”!

📅 2026/7/2 5:49:58
我已经受够了“系统异常”!
作为用户你有没有这样的经验用个软件隔三岔五弹个框系统异常作为程序员你有没有这样的经验运营同学又屁颠屁颠跑来求助“用户不能下单了”“报什么错”“系统异常”无论作为用户还是程序员一见到“系统异常”四个大字我整个人都不好了。它除了告诉我系统出问题了没有任何有价值的信息。这往往是程序员一天苦逼生活的开始。我们获取不到任何有价值的信息只能到处抓虾。先看看系统负载嗯没问题。再看看错误日志一大堆日志滚来滚去也看不出所以然。于是我们不得不求助运营同学“去要一下用户手机号或者账号手机型号、版本最好能录个频”等了半天运营妹妹终于搞来了这些信息于是我们又一顿各种查日志然后盯着代码一行一行找最终发现了 bug 所在。为什么会有“系统异常”喜欢将对外错误信息一股脑写成“系统异常”的一般处于以下几种原因刚入行的小白尚未深入体验程序员的苦难生活。“敏感信息”信徒对他们来说任何系统错误信息都属于敏感信息需要“包装”一下。高敏行业公司强制要求。我见过一些系统是这样处理的class BaseController { errorHandler(err) { this.response.sendJSON({code: 500, message: 系统异常}) } }意思是该系统的所有 throws 都被转成“系统异常”关键还连个日志都不记录后续的开发人员为了方便定位错误便在业务层代码里面各种 log业务代码惨不忍睹。“系统异常”爱好者们的改进措施上面那种极端的代码是比较少见的一般遇到更多的是这样class BaseController { errorHandler(err) { // 生成异常标识并记录日志 let flag random() log(err, flag) this.response.sendJSON({code: 500, message: 系统异常(${flag})}) } }给系统异常后面带了个 flag 标识当出现问题时根据标识就能快速定位日志来排查问题了对于有完善日志系统如 ELK的项目来说已经大大改善了程序员们的生存状况。但上面的代码有什么问题呢试想某支付逻辑有如下代码if (balance amount) { throw new NotEnoughException(卡余额不足) }余额不足很常见的场景但用户看到的是这样的提示“系统异常(1877618)”。此时我不知道用户和程序员有没有崩溃至少你的老板是崩溃的。“系统异常”们的终结“错误码”们横空出现“系统异常”们搞出的事情令人猿共愤如今这些信徒已经不多了要么迫于压力改邪归正了要么被主管开除殆尽了。如今你更可能遇到的是这样的代码配置文件// 全局定义统一的错误码和错误文字 const OK 200 const SYS_ERR 500 const NOT_FOUND 404 const NOT_ENOUGH 405 const map { 200: OK, 500: 系统错误, 404: 未找到资源, 405: 余额不足, } // 错误码转文字 function error(code) { return map[code] }业务层代码... if (balance amount) { // 该自定义异常类仅允许传入错误码内部根据 error() 函数转文字 throw new MyException(NOT_ENOUGH) }控制器class BaseController { errorHandler(err) { log(err) this.response.sendJSON({code: err.code, message: err.message}) // 或者this.response.sendJSON({code: err.code, message: error(err.code)}) } }这种错误处理原则是通过错误码统一整个项目的 code 和 message开发人员不能在程序中自己定义错误描述。我称这类程序员为”错误码“信徒。“错误码”们主要的担心是如果让开发人员自己在代码里面定义错误描述会导致“哈莫雷特”问题即每个人的描述可能都不一样而且有可能会导致敏感信息泄露。相对于“系统异常”们“错误码”们已经有了长足的进步大家终于知道系统发生了什么样的错误老板们也不用担心因客户卡余额不足导致的“系统异常”砸了品牌形象了。从此人猿共欢了从此人猿共欢了用户购买 500 元商品时提示“卡余额不足”但更好的提示应该是“卡余额不足当前可用余额 420.00”。当根据 userId 查不到用户信息时应该提示“用户不存在”但不能保证开发人员因不想定义新 code 而直接使用 404未找到资源。错误码机制的问题是其文字提示过于笼统导致在某些错误场景下丢失重要价值信息进而导致问题排查上的困难问题迟迟得不到解决另一些场景下则带来不好的用户体验。对于开发人员来说它会带来两种效果一些开发人员不想新定义一大堆错误码于是将就着使用现有的错误码导致错误提示不伦不类另外一些开发人员则倾向于定义大量的错误码几乎每处异常都定义一个新错误码理由是每处异常文字提示都不一样最终导致错误码失控。“错误码”们的改进改进其实很简单就是允许异常类传入自定义描述// 增加了可选参数 message允许传入自定义描述 class MyException(code, message ) { ... }期望程序中有如下调用if (balance amount) { throw new MyException(NOT_ENOUGH, 卡余额不足当前可用余额 balance) }但你会惊奇地发现大部分地方仍旧是这样调的if (balance amount) { throw new MyException(NOT_ENOUGH) }“错误码”们忽略了很重要的心理学上的问题。人都是有惰性的如果你提供了偷懒的途径他没有理由不偷懒。反“错误码”们我们追求自由和“系统异常”们以及“错误码”们力求严格限制系统输出不同“自由派”追求极致的自由code 和 message 都不用约束开发人员想怎么写就怎么写。所以你可能在多个地方看到“卡余额不足”的错误但每个的错误码都不同可能是不同的人写的也可能是同一个开发人员在不同时期写的甚至是同一个人在同一天写的写的时候完全看心情。自由派的做法对于错误提示是有好处的开发人员可以尽情地定制个性化的提示内容当系统出现异常时能根据现场提示很快定位错误所在。不过由于错误码是随性写的对于依赖错误码的调用方系统并不友好。一些系统需要依据 API 返回的错误码做一些特殊逻辑处理当调用方认为 405 表示余额不足然而过几天又来个 503 的余额不足时调用方程序员的内心肯定是崩溃的。中庸之道本人的异常处理原则是强制固定 code、自定义 message。要想设计出“人猿共欢”的异常处理机制必须先搞清楚谁需要用到这些信息。异常信息的第一使用者是人这里包括使用者用户和异常处理者运营人员、程序员。细分一下异常又分为业务异常和系统 bug。业务异常是指业务流程中的异常场景如支付时卡余额不足导致无法支付、用券时发现券不符合使用条件、用户执行了某个未授权的操作等。这类异常的触发者是用户自己而不是系统信息受众是用户。所以业务异常的信息提示必须注重用户体验优秀的提示文字至少要做到以下几点尊重用户不要让用户感觉受到冒犯或戏谑请慎用自认为很“幽默”的话语清晰应包含触发异常的关键信息如当余额不足时应提示当前余额是多少具备指引性用户看了之后清楚该怎么做第二类异常是系统 bug如接口超时、非预期参数导致程序崩溃、代码逻辑 bug 等。该类异常的触发者是系统或者说开发系统的程序员信息受众是程序员。所以 bug 类型异常的信息提示必须对程序员友好让程序员看到错误提示后能够快速定位到问题的原因、代码所在的位置。我们说异常一般就是指 bug 型异常这类异常占程序员的精力也是最多的也最值得优化处理机制。bug 型异常具有如下特征不可控性。没有程序员会主动去写 bug但没有哪个系统完全没有 bug。我们无法预知 bug 到底来自哪里、会有什么样的提示信息定位困难。当系统提示“余额不足”时我们很快知道是用户卡没钱了但当系统提示“参数类型错误”时我们往往只能一脸懵逼可能涉及敏感信息。如 SQL 操作错误时可能会将整个 SQL 语句暴露给外界因而优秀的 bug 型异常处理机制应做到提示信息对程序员友好记录函数调用栈信息脱敏。提示信息对程序员友好可能意味着对用户并不友好一些程序员正是据此以“用户体验”之名将 bug 提示信息转换成了“对用户友好”的提示文案结果是所有人看了都云里雾里。我的观点是bug 型异常压根不用考虑用户体验。为啥因为系统出 bug 本身已经是非常糟糕的用户体验了用户不会因诸如“哎呀系统开小差了”之类的废话就变得好受些用户真正关心的是尽快能正常下单。此时的当务之急是快速修复 bug所以提示文案的定位功能就非常重要一段纯技术性的文字对于用户来说可能是天书但对于程序员很实用。然而这不意味着给到用户端的错误提示就可以为所欲为。如果我们为了方便定位便将整个程序调用栈 alert 出来虽然可能并不会进一步拉低用户体验但至少给人的感觉是不专业而且过多的信息也意味着很容易暴露敏感信息如程序路径、软件版本、SQL 语句如果对方是个黑客你只能自祈多福了。另外要注重脱敏。大部分框架在数据库操作失败时其 message 信息中都会包含诸如 SQL 语句之类的敏感信息这类信息不可暴露到外面。综上我们可以采取文案日志的策略文案中包含关键信息日志中包含详细信息包括调用栈信息。大部分的 DB 库抛出的异常都有共同基类如 DBException我们可以针对这类异常做脱敏处理。这也告诉我们另一件事当我们自己开发公共库时最好为该库定义一个统一基类异常这样当使用者想要特殊处理该库抛出的所有异常时不至于狗咬刺猬无处下牙了。另外有些团队并不想记录业务型异常的调用栈信息“卡余额不足”时调用栈信息并无多大意义。我们可以在框架层面定义个业务异常基类BusinessException异常处理时不记录该类型的调用栈信息。异常信息的另一个使用者是系统。包括其他服务、前端 js 脚本等。我见过类似这样的代码try { ... } catch (e) { switch (e.message) { case 用户不存在: ... case ... } }如果某个后端程序员哪天心血来潮将“用户不存在”改成“用户信息不存在”系统就崩了。写出如此脆弱系统的程序员应该被钉到 1024 号耻辱柱上不过在钉钉子之前我们应该倾听一下他那痛苦的心声接口返回的错误码实在是杂乱无章光“用户不存在”的错误码就有八个说不定未来还会增加。为“系统稳定性”考虑最终选择匹配 message。好吧应该将后端程序员一起钉上去系统只会也只应该关注错误码。所以和 message 的随意性不同code 应具备相当的稳定性。同一个系统如果 406 表示“用户不存在”就绝不应该再用其他值如 604表示相同的含义。另外“code 面向系统”这一特点也要求code 定义的是某一类异常而不是某一个异常。例如“订单创建失败”是一类异常在业务代码中针对不同的失败原因有不同的 message但其 code 都是一样的。然而人类对数字并不敏感要不同的程序员都保证写throw new Exception(用户不存在, 406)而不是写throw new Exception(用户不存在, 604)是不可能的。所以需要将数字文本化也就是定义错误码常量const USER_NOT_EXISTS 406代码中只能使用错误码常量throw new Exception(用户不存在, USER_NOT_EXISTS)禁止使用字面量。不过上面这段 throw 并不理想首先默认类型 Exception 并不具备业务语义另外开发人员如果硬是用数字字面量谁也没办法。更可取的方式是针对每种类型异常定义单独的异常类该异常类仅允许传入 message类内部自行绑定 code// 用户不存在 class UserNotExistsException extends Exception { constructor(message) { super(message) this.code ErrCode.USER_NOT_EXISTS } }使用if (!User.find(uid)) { // 此写法更具表达性而且开发人员无需关注错误码 throw new UserNotExistsException(用户不存在(uid:${uid})) }异常捕获机制伪代码示例先总结一下中庸主义的异常捕获机制特点强制开发人员自己编写异常描述文案整个项目强制使用统一的错误码定义为业务型异常定义单独的基类关键信息脱敏处理统一错误码定义const OK 200 const SYS_ERR 500 const NOT_FOUND 404 const NOT_ENOUGH 405 const USER_NOT_EXISTS 406 ...业务异常基类class BussinessException extends Exception { ... }异常类定义class UserNotExistsException extends BussinessException { constructor(message) { super(message) this.code ErrCode.USER_NOT_EXISTS } } ...