跳到主要内容

与错误共处 — 严格模式、测试、调试与异常

这一章讲三件事: 错误从哪来、怎么让它们早点响(strict、类型、测试); 发现程序不对劲时正确的脑子怎么用(理论→观察的调试走查); 以及错误从程序外部漏进来时怎么接(异常、finally、选择性捕获)。 读完你会拥有一套「错误分级处置表」,而不是一句笼统的「多测试」。

1. Bug 从哪来:先分两种

程序员爱把 bug 想象成自己爬进来的小虫;书里拆穿:是我们自己放进去的。然后给了一个有用的二分:程序是结晶的思想——bug 要么因为思想本身糊涂,要么因为想法翻译成代码时走样。前者一般更难诊断:代码照着错的图盖,盖得再整齐也没用1

JS 的宽松(01 章埋的伏笔)在第一种之外又添了第三种麻烦:true * "monkey" 不报错,荒谬的计算悄悄产出 NaN 或 undefined,程序坚信自己在干正事;错误要等坏值穿过好几个函数之后才显形,甚至永不报错、只是默默把输出弄错。这种「错误推迟显形」最难查2

2. 让语言严一点:strict mode

在文件或函数体开头写 "use strict",报错闸门就收紧了一档3:

  • 忘写 let:for (counter = 0; …) 里 counter 没声明——宽松模式悄悄造个全局绑定;严格模式立刻 ReferenceError。注意例外:若同名绑定已存在于作用域中,循环照样悄悄覆盖它4;
  • this 不再兜底:非方法调用的函数里,this 是 undefined 而不是全局对象。书里的对照很生动:Person("Ferdinand") 忘了 new——宽松模式下 name 变成全局绑定还"成功"了;严格模式立刻 TypeError: Cannot set property 'name' of undefined。顺带,class 构造器没 new 必报错,这类事故在 class 时代已经少了一半5;
  • 还禁止同名参数、删掉了 with 这种「错到本书拒绝讨论」的老特性6

类和模块里的代码自动严格——旧的非严格行为只为兼容老代码而存在7

3. 类型:运行前知道更多

有的语言在运行前就要知道每个绑定的类型,类型用得不对当场拦下;JS 只在运行时看类型,还热衷悄悄转换,「帮不上什么忙」8。但类型仍值得写下来——大量错误源于「搞不清进出函数的是什么」,类型注释就是便宜的第一步:

// (graph: Object, from: string, to: string) => string[]
function findRoute(graph, from, to) { … }

类型系统想描述足够多的代码,就得引入自己的复杂度——比如随机取元素的函数,得有类型变量 T 才能写成「从 T 数组到 T」9。想更进一步,书里直接推荐 TypeScript,然后补了一句很有性格的话:「本书将继续使用原始、危险、无类型的 JavaScript。」——教材的取舍,不是对读者的建议10

4. 测试:写一遍,永久生效

语言不管,就只能自己找错。手测是最坏方案:烦,而且永远测不全。自动测试=写程序测程序;前期多花点功夫,之后获得的是超能力级的反馈:几秒内验证所有写过的场景,改坏东西当场尖叫11

测试长什么样?书里用 20 行自己搭了个框架:

function test(label, body) {
if (!body()) console.log(`Failed: ${label}`);
}
test("拉丁字母转大写", () => "hello".toUpperCase() == "HELLO");
test("希腊字母转大写", () => "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ");
test("无大小写的文字不动", () => "مرحبا".toUpperCase() == "مرحبا");

成套的测试通常交给**测试运行器(test runner)**管理12。还有一条选型洞察:代码交互的外部对象越多越难测(要搭的环境越多);上一章那种「自包含的持久值」风格,天然好测——风格选对了,测试成本先降一半13

5. 主走查:调试 = 理论,然后观察

程序出了错,下一步最难。书里先立了铁律:抵抗「乱改代码碰运气」的冲动;要思考——分析现状,提出一个「为什么会这样」的理论,再做观察去验证或逼出理论14

演示用的病灶(书里原例):把整数转成指定进制的字符串。

function numberToString(n, base = 10) {
let result = "", sign = "";
if (n < 0) { sign = "-"; n = -n; }
do {
result = String(n % base) + result;
n /= base;
} while (n > 0);
return sign + result;
}
numberToString(13, 10); // → "1.5e-3231.3e-3221.3e-321…" ???

走查全程:

症状 numberToString(13, 10) 应得 "13",实际输出一串科学计数法
理论 结果里出现小数点 → n 在中途不再是整数 → n /= base 在做除法
观察 在循环开头打印 n,期望 13、1、0,实际得到:
13
1.3 ← 第一轮就坏了
0.13
0.013 … 1.5e-323
诊断 13/10 = 1.3。「去掉末位」要的是整除,而 /= 是真除法
处方 n = Math.floor(n / base) —— 除完向下取整,把数「右移一位」
复验 n: 13 → 1 → 0,循环正常终止,输出 "13"

注意这套流程的结构:症状 → 理论(除法没取整)→ 观察(打印 n 的序列)→ 处方 → 复验。观察手段除了策略性 console.log,还有断点(breakpoint)与 debugger 语句——程序跑到那行暂停,你可以翻看每个绑定的当前值15

6. 异常:跳楼,以及楼下的网

程序外的失败(坏输入、网络断、文件没了)防不胜防。处置的第一招是返回特殊值(null、-1),它适合「错误常见、调用方理应过问」的场合,但有两个硬伤16:

  1. 函数本来就可能返回任何值时,特殊值没地方站——只能包一层对象(书里的 lastElement 空数组返回 {failed: true};迭代器的 next() 用 {value, done} 同理);
  2. 调用十次就得判十次 null,而且每层调用者都得继续接力。

异常(exception)是另一条路:出问题的代码 throw 一个值,控制流立刻向上跳,跳过当前函数、跳过它的调用者,一路跳到最外层——书里管这叫展开调用栈(unwinding the stack),上一章的调用栈被它一路丢弃17。当然,总跳到楼底就只是花式崩溃;它的力量在于你可以沿栈设「障碍」:

try {
console.log("You see", look()); // look 内部可能 throw
} catch (error) {
console.log("Something went wrong: " + error);
}

Error 构造器造的异常自带 message 和 stack trace(抛出瞬间的调用栈快照),是排查的头号线索18。中间层函数由此获得一项实打实的红利:错误处理只写在「出错点」和「处理点」,中间所有函数完全免责——look 根本不知道 promptDirection 会抛错19

异常的暗面:半途而废的副作用

异常是第三种控制流:每一次函数调用、每一次属性访问,都可能让控制流突然离开你的代码。书里用一段「真·银行代码」演示:transfer 先扣款、再询问对方账户——若询问时抛了异常,钱已经扣了,却永远没到账20。三层防御,按优先级:

  1. 少副作用:算新值而不是改旧值——写一半死掉,旧数据毫发无损(本章与全书反复出现的原则);
  2. 实在要改,把「先问后动」的顺序理顺(问名再转账);
  3. 顺序救不了时,用 finally:无论 try 怎么退出都会执行。书里的版本用 progress 变量记进度,finally 里发现「死在扣款之后」,就把钱加回去——finally 不拦截异常,跑完继续向上飞21

作者对这部分的态度罕见地悲观:写出对异常处处稳健的程序非常难,很多人干脆不做;异常又专挑罕见时刻,问题可能永远不被发现。好不好?取决于这软件挂掉时会造成多大伤害22

7. 选择性捕获:别一网打尽

没人接的异常交给环境:浏览器写进控制台,Node 直接中止整个进程。书里的分级很清楚:程序员的错误,让它崩(合理的崩=明确的信号);日常使用中预期的失败,崩就是灾难23

麻烦在于:JS 不能按类型选择性捕获——要么全抓,要么不抓。书里把这列为语言「相当扎眼的一个缺失」。于是一个循环里的拼写错误(promtDirection 少了个 p)抛出「未定义绑定」,被全收的 catch 误判成「用户输入无效」,结果是:无限循环,外加真错误被埋葬24。由此得出通用法则:除非是为了把异常「路由」到别处,否则不要一网打尽25

想精确捕获又不想比对 message 字符串(那是给人看的,一改就崩),标准做法是自定义异常类型 + instanceof:

class InputError extends Error {}
// 抛:throw new InputError("Invalid direction: " + result)
// 接:catch (e) { if (e instanceof InputError) …重试; else throw e; }

不认识的异常重新抛出去,拼错的变量名这回能正常报错了26

8. 断言:为「程序员的错」服务的爆炸

断言(assertion)= 程序内部的检查,验证「这里本该如此」。它不是为处理正常运营中的意外,而是为揪程序员的错:firstElement 承诺只对非空数组调用,那就写成空数组当场 throw——宁可大声炸掉,不要悄悄返回 undefined 让错误漂向下游27。别过头:给每种坏输入都写断言,工作量大、代码吵,留给「容易犯、或你已经犯过的错」即可28

9. 作者的判断与证据

  • 章首引 Kernighan:「调试的难度是写代码的两倍;所以你用尽全力写出的聪明代码,按定义已经超出你能调试的范围」——本章的价值观总纲29;
  • 「严格模式很少有害、可能有益」:低成本高收益的经验判断30;
  • 「本书继续用原始危险的 JS」:教材取舍,明确说明;
  • 「异常稳健性看伤害定投入」:给出决策变量(失败代价),不是一刀切22

判断(我们的,不是书里的): 这一章的方法论可以直接映射到「错误分级处置表」: 程序员的错 → 断言/严格模式,让它响;可预期的外部失败 → 特殊值或自定义异常,让它可处理; 不知道哪来的异常 → 别抓,或抓了必 rethrow。多数「吞错误」的事故,都是把第三类按第一类处理了。 如果错,会错在: 高可用服务里「让它崩」可能不成立(崩溃本身就是事故),那正是 retry/隔离层存在的理由——分级表要以部署环境为前提。

10. 边界与局限

  • TypeScript 只有一段推荐语,类型系统本体不在本书(书里也自陈不会讲);
  • 测试只搭了 20 行的玩具框架,断言库、mock、覆盖率都没有;
  • 「典型实现里」的性能与栈行为随引擎变化;
  • 例子里的 prompt/alert 属于演示用 API,真实界面不该用(书在后文也用对话框只做演示)。

11. 可带走的

  1. bug 二分:想法错 vs 翻译错,前者更难;JS 的宽松让错误被推迟显形;
  2. "use strict":未声明绑定报错、this 不兜底;类与模块自动严格;
  3. 类型注释是零成本文档;要机器查就上 TypeScript;
  4. 自动测试=超能力;外部依赖越少越好测——持久值风格顺手得分;
  5. 调试铁律:先理论后观察,禁止乱改;numberToString 走查的五步可以背;
  6. 异常=栈展开;中间函数免责;Error 自带 stack trace;
  7. 多副作用代码遇到异常会「钱没了」;先算新值,再谈 finally 回滚;
  8. JS 不能选择性捕获——instanceof + rethrow 是标准姿势,别比对 message;
  9. 断言为程序员的错服务:宁可当场炸,不悄悄返回 undefined;
  10. 投入多少做异常稳健,取决于失败时的伤害

12. 原文地图

主题原书章原文位置
bug 二分:想法/翻译Bugs and Errorstext/11-fm-bugs-and-errors.txt:11(搜「crystallized thought」)
宽松让错误推迟显形同上text/11-fm-bugs-and-errors.txt:19(搜「happily continues」)
strict mode 基本用法同上text/11-fm-bugs-and-errors.txt:25(搜「use strict」)
忘 let 与已有绑定例外同上text/11-fm-bugs-and-errors.txt:39(搜「quietly creates a global」)
Person 忘 new 对照同上text/11-fm-bugs-and-errors.txt:46(搜「Ferdinand」)
类自动严格、删 with同上text/11-fm-bugs-and-errors.txt:37(搜「automatically strict」) · :61(搜「with statement」)
类型注释与类型变量同上text/11-fm-bugs-and-errors.txt:71(搜「findRoute」) · :80(搜「type variable」)
推荐与拒绝同上text/11-fm-bugs-and-errors.txt:82(搜「TypeScript」) · :84(搜「raw, dangerous」)
自动测试超能力同上text/11-fm-bugs-and-errors.txt:92(搜「superpower」)
迷你测试框架与三例同上text/11-fm-bugs-and-errors.txt:94(搜「toUpperCase」) · :104(搜「Χαίρετε」)
好测的风格同上text/11-fm-bugs-and-errors.txt:112(搜「persistent values」)
抵抗乱改同上text/11-fm-bugs-and-errors.txt:141(搜「random changes」)
numberToString 病灶同上text/11-fm-bugs-and-errors.txt:124(搜「numberToString」) · :137(搜「1.5e-323」)
观察序列与处方同上text/11-fm-bugs-and-errors.txt:137(搜「1.3」) · :152(搜「Math.floor」)
断点与 debugger 语句同上text/11-fm-bugs-and-errors.txt:154(搜「breakpoint」) · :156(搜「debugger」)
特殊值的两个硬伤同上text/11-fm-bugs-and-errors.txt:178(搜「downsides」) · :188(搜「10 times」)
异常与栈展开同上text/11-fm-bugs-and-errors.txt:194(搜「unwinding」)
栈上的障碍同上text/11-fm-bugs-and-errors.txt:196(搜「obstacles」)
Error 与 stack trace同上text/11-fm-bugs-and-errors.txt:223(搜「stack trace」)
中间函数免责同上text/11-fm-bugs-and-errors.txt:225(搜「forget all about it」)
银行转账丢钱同上text/11-fm-bugs-and-errors.txt:259(搜「disappear」)
finally 回滚同上text/11-fm-bugs-and-errors.txt:269(搜「progress」) · :284(搜「continues unwinding」)
投入看伤害同上text/11-fm-bugs-and-errors.txt:286(搜「damage」)
让它崩 vs 崩是灾难同上text/11-fm-bugs-and-errors.txt:292(搜「broken program」) · :294(搜「terrible strategy」)
全收的代价(typo 循环)同上text/11-fm-bugs-and-errors.txt:306(搜「promtDirection」) · :314(搜「buries」)
不一网打尽同上text/11-fm-bugs-and-errors.txt:316(搜「blanket-catch」)
InputError + instanceof同上text/11-fm-bugs-and-errors.txt:324(搜「InputError」) · :343(搜「instanceof」)
断言同上text/11-fm-bugs-and-errors.txt:355(搜「programmer mistakes」) · :368(搜「noisy」)
Kernighan 题词同上text/11-fm-bugs-and-errors.txt:5(搜「twice as hard」)

Footnotes

  1. 出处:「Bugs and Errors」第 9 段(text/11-fm-bugs-and-errors.txt:9,搜「crawl into」)与第 11 段(text/11-fm-bugs-and-errors.txt:11,搜「crystallized thought」)。

  2. 出处:「Bugs and Errors」第 19 段(text/11-fm-bugs-and-errors.txt:19,搜「happily continues」)。

  3. 出处:「Bugs and Errors」第 25 段(text/11-fm-bugs-and-errors.txt:25,搜「strict mode」)。

  4. 出处:「Bugs and Errors」第 39 段(text/11-fm-bugs-and-errors.txt:39,搜「quietly creates a global」)。

  5. 出处:「Bugs and Errors」第 41 段(text/11-fm-bugs-and-errors.txt:41,搜「global scope object」)与第 55 段(text/11-fm-bugs-and-errors.txt:55,搜「Cannot set property」)。

  6. 出处:「Bugs and Errors」第 61 段(text/11-fm-bugs-and-errors.txt:61,搜「with statement」)。

  7. 出处:「Bugs and Errors」第 37 段(text/11-fm-bugs-and-errors.txt:37,搜「automatically strict」)。

  8. 出处:「Bugs and Errors」第 67 段(text/11-fm-bugs-and-errors.txt:67,搜「before even running」)。

  9. 出处:「Bugs and Errors」第 80 段(text/11-fm-bugs-and-errors.txt:80,搜「type variable」)。

  10. 出处:「Bugs and Errors」第 82 段(text/11-fm-bugs-and-errors.txt:82,搜「TypeScript」)与第 84 段(text/11-fm-bugs-and-errors.txt:84,搜「raw, dangerous」)。

  11. 出处:「Bugs and Errors」第 92 段(text/11-fm-bugs-and-errors.txt:92,搜「superpower」)。

  12. 出处:「Bugs and Errors」第 96 段(text/11-fm-bugs-and-errors.txt:96,搜「test」)与第 110 段(text/11-fm-bugs-and-errors.txt:110,搜「test runners」)。

  13. 出处:「Bugs and Errors」第 112 段(text/11-fm-bugs-and-errors.txt:112,搜「persistent values」)。

  14. 出处:「Bugs and Errors」第 141 段(text/11-fm-bugs-and-errors.txt:141,搜「random changes」)。

  15. 出处:「Bugs and Errors」第 143 段(text/11-fm-bugs-and-errors.txt:143,搜「strategic」)与第 154 段(text/11-fm-bugs-and-errors.txt:154,搜「breakpoint」)。

  16. 出处:「Bugs and Errors」第 176 段(text/11-fm-bugs-and-errors.txt:176,搜「special value」)与第 178 段(text/11-fm-bugs-and-errors.txt:178,搜「downsides」)。

  17. 出处:「Bugs and Errors」第 194 段(text/11-fm-bugs-and-errors.txt:194,搜「unwinding the stack」)。

  18. 出处:「Bugs and Errors」第 223 段(text/11-fm-bugs-and-errors.txt:223,搜「stack trace」)。

  19. 出处:「Bugs and Errors」第 225 段(text/11-fm-bugs-and-errors.txt:225,搜「forget all about it」)。

  20. 出处:「Bugs and Errors」第 259 段(text/11-fm-bugs-and-errors.txt:259,搜「disappear」)。

  21. 出处:「Bugs and Errors」第 265 段(text/11-fm-bugs-and-errors.txt:265,搜「finally」)与第 284 段(text/11-fm-bugs-and-errors.txt:284,搜「continues unwinding」)。

  22. 出处:「Bugs and Errors」第 286 段(text/11-fm-bugs-and-errors.txt:286,搜「damage」)。 2

  23. 出处:「Bugs and Errors」第 290 段(text/11-fm-bugs-and-errors.txt:290,搜「aborts the whole process」)与第 294 段(text/11-fm-bugs-and-errors.txt:294,搜「terrible strategy」)。

  24. 出处:「Bugs and Errors」第 300 段(text/11-fm-bugs-and-errors.txt:300,搜「glaring omission」)与第 314 段(text/11-fm-bugs-and-errors.txt:314,搜「buries」)。

  25. 出处:「Bugs and Errors」第 316 段(text/11-fm-bugs-and-errors.txt:316,搜「blanket-catch」)。

  26. 出处:「Bugs and Errors」第 324 段(text/11-fm-bugs-and-errors.txt:324,搜「InputError」)与第 351 段(text/11-fm-bugs-and-errors.txt:351,搜「let unrelated exceptions」)。

  27. 出处:「Bugs and Errors」第 355 段(text/11-fm-bugs-and-errors.txt:355,搜「programmer mistakes」)与第 366 段(text/11-fm-bugs-and-errors.txt:366,搜「loudly」)。

  28. 出处:「Bugs and Errors」第 368 段(text/11-fm-bugs-and-errors.txt:368,搜「noisy」)。

  29. 出处:「Bugs and Errors」第 5 段(text/11-fm-bugs-and-errors.txt:5,搜「twice as hard」)。

  30. 出处:「Bugs and Errors」第 63 段(text/11-fm-bugs-and-errors.txt:63,搜「rarely hurts」)。