12 错误的旅程:从 throw 到全局兜底
12 错误的旅程:从 throw 到全局兜底
这一章讲什么
错误对象造好了、信息也全,但送不到能用它的地方,一切都白搭。
这一章跟踪一次 throw 的完整旅程:控制流怎么被打断、调用栈怎么逐帧拆除、
catch 该放在哪(答案是「按含义划分的边界」)、finally 这个老实人怎么会变成内鬼;
然后是本章的重头:异步边界——setTimeout 这类「晚点再跑」的机制
会拆掉整个调用栈,try-catch 在它面前失效,栈里再也查不到「是谁排的这个任务」。
最后看兜底的最后一道防线:全局错误处理器,以及为什么它只配当安全网。
顶层全景:一次 throw 的三种结局
throw:正常执行立即停止,引擎沿调用栈向上逐帧拆,找 catch
│
├─ ① 某个 try-catch 接住了 → 在这里做恢复/记录决策,传播终止
│ (接住了还可以用 cause 包好再抛,送往上一层边界)
│
├─ ② 一路拆到栈底没人接 → 交给宿主:浏览器打一条控制台日志;
│ Node.js 终结进程(uncaughtException 事件,视为致命)
│
└─ ③ 错误发生在异步回调里 → 调用栈早已消失,①②都够不着
→ 只能落到全局错误事件;恢复上下文全丢,栈只剩定时器内部帧
图说:三个结局里,只有①给了你完整的恢复上下文和调试现场;
这一章的全部实践建议,就是「尽量让错误在①里被接住」。核心原理
3.1 throw 是劫机,不是创建
先纠正一个常见混淆:throw 不创建错误——new Error(...) 才创建。
throw 做的是劫持控制流:语句执行到它的那一刻,正常流程立即停止,
引擎开始沿着调用栈往回走,一层一层拆,找能处理异常的 catch;
每拆一层,那层函数剩下的代码全部不再执行1。
书里给的最小示例,状态清清楚楚2:
function level3() { throw new Error("Origin point"); }
function level2() { level3(); console.log("Never executed"); }
function level1() { level2(); console.log("Never executed"); }
level1(); // 报错:Origin point —— 两层 console.log 都没跑栈的拆除是「逐帧上卷」(unwind):level3 抛出 → level3 帧拆 →
level2 帧拆(它的 console.log 没执行)→ level1 帧拆 → 到顶层还没有
catch → 报告宿主:未捕获3。catch 一旦出现,拆除就停在那里,
「已捕获」成立,传播终止4。
3.2 catch 放哪:语义边界是唯一正解
catch 的位置有个天然张力,书里摆得很明白5:
- 放太深(离 throw 近):本地调试信息全在,但你往往没有 做恢复决策的信息——底下某层数据库查询失败,它自己不知道该提交还是回滚;
- 放太高(接近顶层):恢复决策的信息够了,但「具体哪一步、为什么失败」 的细节已经在层层上传中磨没了。
正解:放在语义边界(semantic boundary)上——你既有足够的信息做恢复、 又能为上层保留有用细节的点位6。书里列了四类7:
| 边界 | 例子 | 恢复决策 |
|---|---|---|
| 事务(一组要么全成、要么全不成的操作)边界 | 一组写库操作整体提交或回滚 | 失败 → rollback |
| API 边界 | 调外部服务、第三方库 | 失败 → 重试/降级/熔断 |
| 用户操作边界 | 一次点击触发的完整动作 | 失败 → 告知用户、局部回滚 |
| 资源(文件句柄、连接这类系统物件)边界 | 获取/释放文件、连接 | 失败 → 清理、稍后重试 |
主线例子:数据库事务。 三条查询加一次提交,包在一个 try 里:
function updateUserProfile(userId, changes) {
const tx = db.beginTransaction();
try {
const user = tx.query('SELECT ...', userId); // 可能失败
tx.query('UPDATE ...', userId, changes); // 可能失败
tx.query('INSERT INTO audit_log ...', userId); // 可能失败
tx.commit();
} catch (e) {
tx.rollback(); // 恢复决策:回滚
throw new Error('Profile update failed', { cause: e }); // 包好,送上边界
}
}为什么不每条查询各自包 try-catch?单条查询失败时你不知道该不该回滚——
那要到「整组操作」这一层才知道;也不该把 catch 放到应用顶层——那时你已经丢了
「是哪条查询失败」的细节。事务边界本身就是那个刚刚好的位置:
在这里回滚是合法的,用 cause 把原始错误包进新错误往上送,
上层边界拿到的是干净的恢复语义 + 完整的调试链8。
3.3 finally:永远会跑,也会永远坑你
finally 块是 try-catch 的收尾员:无论有没有抛错、有没有被接住,
它都执行——这是它做资源清理(关连接、释放文件)的价值所在9。
但书里给这个老实人揭了底:finally 里的代码可以吞掉或替换正在传播的
错误——这是规范明文规定的行为,不是 bug10:
function suppressed() {
try { throw new Error("This disappears"); }
finally { return "Success!"; } // return 劫持:错误被吞,函数正常返回!
}
function replaced() {
try { throw new Error("This disappears"); }
finally { throw new Error("This replaces it"); } // 新错误顶掉旧的
}原错误对象其实还活着,栈也抓了,消息也设了——但传播途中被 finally 截胡,一切白搭,恢复与调试两头的信息全部作废11。 作者的评价:这是规范有意给开发者的最大控制权,但对不小心的人, 它是上了膛的脚枪(footgun)12。纪律只有一条:finally 里只做清理, 绝不 return、绝不 throw。
3.4 用 throw 当流程控制?绝对不行
书里单独立了块牌子:「绝不要用 throw 做控制流」13。
反模式长这样:在 forEach 回调里 throw item,外层 try-catch 接住当
返回值用——把异常机制当 goto 使14。
历史上这招是真性能的灾难(V8 时代早期优化不了 try-catch);
如今引擎能优化了,它仍然是烂代码:性能账另算,可读性和可调试性已经完了。
该用 return/break/continue 的地方就用它们;
throw/try/catch 只留给真正的异常15。
3.5 异步边界:调用栈消失的地方
本章重头。01 章建立的事实在这里兑现:setTimeout、process.nextTick、 事件监听、Promise 回调——所有「晚点再跑」的机制,都切断了调用栈。 看一个看似无害的例子16:
function processData(data) {
setTimeout(() => { throw new Error("Processing failed"); }, 100);
}
try {
processData(data);
} catch (e) {
console.log("Never catches it"); // 永远打不出来
}外层的 try-catch 完全够不着这个错误。原因在时序:100 毫秒后回调 真正执行时,当初那个包含 try-catch 的调用栈早已跑完、消失; 错误被抛在一个全新的栈里,这个新栈和当初调度它的代码没有任何连接17。 错误没有走向「被接住」或「交给宿主」的正常路径,而是直接砸进全局。
它同时砸断了恢复和调试两头18:
- 恢复侧:边界断了。调度异步工作的代码没法用 try-catch 捕获回调里的失败; 你必须有完全不同的机制跨这个边界传错误(Promise 链就是答案之一—— 但 01 章说过,microtask 队列的事,这本书承诺的第 8 章还没写)。
- 调试侧:栈残缺。抛错瞬间的栈里只有回调内部和定时器实现, 没有任何「是 processData 调度了我」的信息19:
Error: Processing failed
at Timeout._onTimeout (file.js:3:11)
at listOnTimeout (node:internal/timers:573:17)
at process.processTimers (node:internal/timers:514:7)三行全是定时器内部机制,你的业务函数一个字没有20。
补丁的存在与极限:现代引擎有「async stack traces」(异步栈),
尝试重建「await 链、Promise 链」来补全历史;但重建的栈不完整、有性能代价、
且根本不解决恢复问题——最致命的是,它对 setTimeout 这类 Web API
完全无效,因为引擎根本不知道宿主调度了它21。
书里给这条边界下的判词值得记住:异步边界从根本上把错误和它的来源拆开了; 任何「把工作排晚点」的 API 都跨过了这条线,你在 01 章学的所有调度机制 都是越境点22。
3.6 全局处理器:最后一道防线,不是策略
try-catch 够不着的错误总得有个去处:引擎找遍栈无果,会把错误交给宿主, 宿主是绝对最后一道防线23。两边的形态:
浏览器:window.addEventListener('error', ...)——能接到同步和异步代码里
所有漏网之鱼;回调里拿得到错误对象、出错的文件名和行号,
event.preventDefault() 还能阻止默认的报错行为24。
Node.js:process.on('uncaughtException', ...)。关键差异:
在 Node 里未捕获异常是致命的——进程已处于未定义状态,不应继续正常工作;
没有这个处理器,进程会打日志然后退出25。
两个用途都是真的:记日志、报监控。但两边的能力都有限,书里点名了26:
- 对恢复:全局处理器离现场太远。错误到这时,你不知道当时在做什么操作、 涉及什么数据、系统处于什么状态、该用什么恢复策略——能做的只有记录,然后祈祷;
- 对调试:同步错误的栈虽完整,但已经穿过不知多少层抽象; 异步错误的栈照旧残缺。全局处理器是「错误被记录的地方」, 不是「错误被理解的地方」27。
所以定调:真正的解法是别让错误走到这一步——在语义边界上接住它们; 全局处理器是安全网,用来兜「错误处理本身的失误」。 如果错误经常落到全局处理器,信号很明确:边界的错误处理缺位了28。
3.1b 旅程的逐帧拆解:三层函数怎么被拆掉的
3.1 节给了最小示例,这里把「逐帧拆除」走成慢动作29:
栈(自下而上): [level1] [level2] [level3]
level3 抛出 ──────────────────────────────→ 先拆 level3 自己:
它剩下的语句作废
[level1] [level2] → 拆 level2:
它的 console.log("Never executed") 不跑
[level1] → 拆 level1:
它的 console.log("Never executed") 也不跑
栈空,无 catch ──────────────────────────────→ 报告宿主:uncaught每一步拆除做两件事:丢弃该层剩余的字节码执行,释放该层的栈帧。 所以 throw 的代价不只是「造了个 Error 对象」,还有沿途每一层的 「善后」——层数越深,拆除越贵;这也是为什么「用 throw 当流程控制」 (3.4 节)既难看又真贵:它在为「正常的返回路径」付「异常的拆除费」30。
catch 块的位置决定了拆除在哪一层停。同一个错误,被 middle 接住与被 outer 接住,对系统的意义完全不同:middle 接住,middle 以上的层 (包括 outer 的 try)都感知不到任何事;outer 接住, 中间所有层的本地状态已经全部灰飞烟灭——这就是为什么「放哪一层」 是个有实质后果的决策,而不是风格问题31。
3.2b 四类边界的完整定义与判据
3.2 节列了四类语义边界,这里给每类一个「为什么是它」的判据32:
| 边界 | 判据:为什么恢复决策在这里才合法 |
|---|---|
| 事务边界 | 提交/回滚是针对「整组操作」的语义,单条查询失败时无法决定——必须等整组收尾 |
| API 边界 | 重试/降级/熔断的依据是「这次外部调用」的合约(幂等吗?有备用端点吗?),只有调用方知道 |
| 用户操作边界 | 「告诉用户失败了、保留他已填的表单」需要 UI 上下文,深处没有 |
| 资源边界 | 释放/重试的决策需要「这个资源现在归谁管」的知识,获取处最清楚 |
反过来,两个反例也成立:在单条查询处 catch——你不知道该不该回滚,
因为组里还有三条没跑;在应用顶层 catch——你知道该「记日志、
返回 500」,可「具体哪一步、为什么失败」已经在四层拆除里磨没了33。
事务例程里那个 throw new Error('Profile update failed', { cause: e })
的写法,是「边界上抛干净的 + 链条上保完整的」的标准动作:
外层拿到一句人话,内层的原始错误躺在 cause 里跟着走34。
3.3b finally 的全规则与两个反例的展开
finally 的规则比「总是执行」多一条关键的:它能劫持传播中的错误。 原书的两个反例,展开看状态35:
function suppressed() {
try {
throw new Error("This disappears");
} finally {
return "Success!"; // return 先发生:错误对象还在,但传播被打断
} // 调用方收到 "Success!",毫无异常迹象
}
function replaced() {
try {
throw new Error("This disappears");
} finally {
throw new Error("This replaces it"); // 新错误顶替旧错误上塔
} // 调用方只看得见新的
}原错误对象其实还活着——栈抓了、消息设了——但传播途中被 finally 截胡,一切白搭,恢复与调试两头的信息全部作废36。 书里给这种行为的定性是:规范有意给开发者的最大控制权, 本意是让资源清理不被异常打断;可对不小心的人,它是上了膛的脚枪37。 纪律因此只有一条:finally 里只做清理,绝不 return、绝不 throw。 需要「清理后还要抛」的场景,把清理放 finally、把抛留在外面:
let tx;
try {
tx = db.beginTransaction();
tx.query('...');
tx.commit();
} catch (e) {
tx.rollback(); // 恢复动作
throw new Error('update failed', { cause: e }); // 重抛放在 catch 里
} finally {
tx.close(); // finally 只负责关门
}3.4b 「别拿 throw 当流程控制」的完整论证
原书把这条禁令写成了带历史背景的论证,值得照录38。反模式长这样:
function findItem(array, test) {
try {
array.forEach((item) => {
if (test(item)) throw item; // 把 throw 当 break 使
});
} catch (result) {
return result;
}
}「找到就抛、外层接住当返回值」——把异常机制当 goto。
历史上这招是真性能的灾难:早期的 V8 优化不了 try-catch,
这种写法的性能可以直接杀死应用;如今引擎能优化了,
它仍然是烂代码:可读性、可调试性、维护性全线崩塌39。
规则书里写得很死:该用 return/break/continue 的地方就用它们;
throw/try/catch 只留给真正的异常40。
一句话版本:异常是「计划外的出口」,不是「方便的跳转」。
3.5b 异步断层的完整时序图
3.5 节用文字讲了「调用栈消失」,这里把时序摆开—— 这是本章最重要的一张图:
主栈时间线 ──────────────────────────────────────────►
try { ← 包含 try-catch
processData(data) ──► setTimeout(cb, 100) ← 登记回调,不执行
} catch (e) { ... } ← 跟着主栈一起结束
[主栈销毁] ← 100ms 前就已不存在
100ms 后,事件循环启动一个全新的栈:
cb() ──► throw new Error(...) ← 栈上只有定时器内部帧
↑ 这个栈与当初调 processData 的栈没有任何连接
↑ try-catch 是上一个栈的遗物,物理上够不着错的不只是「catch 够不着」:栈里也只剩新栈的三行——
Timeout._onTimeout、listOnTimeout、processTimers,
没有任何一行告诉你「这是 processData 排的队、排它是为了处理什么数据」41。
恢复侧与调试侧在这张图上是同一场塌方:错误的「来源上下文」
(谁排的、为什么排)随旧栈一起死了,剩下的只有一个裸错误
掉在全新的世界里——它能落进的地方只有全局处理器。
3.6b 全局处理器的能力边界:一张对照表
浏览器 window.addEventListener('error', ...) | Node.js process.on('uncaughtException', ...) | |
|---|---|---|
| 能接到 | 同步 + 异步的所有漏网错误 | 同步 + 异步的所有漏网错误 |
| 拿到什么 | 错误对象、文件名、行号;preventDefault() 可阻止默认报错 | 错误对象 + origin(同步/异步来源) |
| 接完默认行为 | 控制台报错(可被 preventDefault 阻止) | 没有处理器才退出;有了处理器进程继续,但状态已未定义 |
| 定位 | 兜底日志/监控的最后一站 | 同左;Node 官方定性为「致命」,处理完应尽快退出42 |
两个用途都是真的:记日志、报监控。但两个局限同样硬—— 对恢复:全局处理器离现场太远,错误到这时,不知道当时在做什么操作、 涉及什么数据、系统处于什么状态,能做的只有记录加祈祷43; 对调试:同步错误的栈虽完整,可已经穿过不知多少层抽象; 异步错误的栈照旧残缺。书里那句定性值得背: 「全局处理器是错误被记录的地方,不是错误被理解的地方。」44
所以最终定调只有一句:真正的解法是在语义边界上接住错误, 别让它们走到这一步;全局处理器是安全网,用来兜「错误处理本身的失误」。 错误经常落到全局处理器,信号很明确——边界的错误处理缺位了45。
3.7 本章的章末总结(原书十条)
原书第 7 章结尾给了十条总结,照录作核对单46:
- 可以 throw 任何值,但带栈的 Error 对象是恢复与调试两用的约定俗成;
- 原生类型(TypeError/RangeError)描述语言违规,不给恢复指导; AggregateError 与 DOMException 的语义类型更实用;
- cause 支持边界处包错误:对外干净、对内保完整链;
- Node.js 错误码把机器恢复与人读消息分开,告别了脆弱的「匹配消息」;
- 调用栈为调试抓现场,代价是实打实的内存与 CPU;惰性序列化与限深度是对策;
- try-catch 的传播是逐帧拆除,直到找到处理器或撞毁;
- catch 放语义边界:事务、API、用户操作、资源;
- finally 保证清理,但 return/throw 会吞错换错;
- setTimeout 等异步操作拆掉调用栈,try-catch 失效,栈丢失调度上下文;
- 全局处理器是兜底网;错误常落到那里 = 边界处理缺位。
十条里有六条是「不要做什么」——错误处理这个主题,禁令比技法多。 这倒符合它的本质:错误路径是平时走不到、出事时救命的路, 它的每一处分岔都要在出事前就定好。 而定分岔的依据,就是 「恢复要上下文、调试要现场、两者成本对立」这条主线。
3.7b 与第 1 章的闭环:队列里的错误没人接
把第 1 章的队列图和本章的旅程图叠在一起,一个完整的画面就出来了: 错误在同一次调用栈里 throw 时,try-catch 是有效的——它们共享同一摞帧; 错误在另一个任务里 throw 时,两个任务的栈中间隔着整个事件循环, try-catch 物理上够不着47。所以「异步边界断裂」不是错误系统的缺陷, 而是调度系统的必然:01 章学的每一个队列(microtask、nextTick、 immediates、定时器),都是一道栈的断层线;错误每跨过一条, 就要么靠 Promise 链那套「平行的栈」接力,要么掉进全局处理器48。
这条闭环也解释了第 1 章那个反常的断言——「微任务队列和事件循环 没有直接关系」49:微任务是语言定义的队列,宿主想什么时候清就什么时候清; 而错误传播恰恰依赖「同一摞帧」,宿主对队列的自由裁量, 等于对错误传播路径的自由裁量。调度的自由,就是错误处理的代价—— 同一个硬币的两面,被这本书拆在两章里讲完。
作者的判断与证据
有证据的判断:
- try-catch/finally/setTimeout 的行为演示都是可运行的代码, 「Never executed」「Never catches it」这些输出都可直接复现50;
- 「Node 视未捕获为致命」是 Node 文档明载的行为51。
作者的主张:
- 「catch 放语义边界」是本 书的核心实践主张之一; 「四类边界」清单是作者经验归纳,不是规范分类。
- 「全局处理器只配当安全网」是立场鲜明的定调; 现实中(尤其在短生命周期脚本、CLI 工具里)把 uncaughtException 当主策略用的代码不少,书里没讨论这些场景的宽容度。
边界与局限
- Promise/async 的错误传播(本书多次预告的「第 8 章」)在 MEAP 里 不存在——异步错误机制的正确答案这一章故意只开了个头; async/await 如何用 try-catch 找回同步感,要等最终版。
- async stack traces 的具体能力(V8 的
--async-stack-traces及默认行为) 书中只给了一段落,细节随版本变化。 - 浏览器
unhandledrejection事件(Promise 专属兜底)全书未提—— 它是和 uncaughtException 平行的另一半兜底体系。 - Worker/跨线程的错误传播(postMessage 传不了 Error 等)不在范围。
可带走的
- throw 劫持控制流、沿栈逐帧拆除;catch 在哪,拆除就停在哪。
- catch 放深了下不了恢复决策,放高了丢调试细节——放语义边界上。
- 四类边界:事务、API、用户操作、资源;事务示例的模式(rollback + cause 重抛)直接可抄。
- finally 永远执行,但 return/throw 会吞掉或替换原错误;finally 只清理。
- throw 不是流程控制;return/break/continue 干它们该干的事。
- setTimeout 等异步回调里抛错,外层 try-catch 永远接不住——栈已经没了。
- async stack traces 补调试不补恢复,且对 Web API 调度完全无效。
- 全局处理器(浏览器 error 事件 / Node uncaughtException)只配当安全网; 错误常落到这里 = 边界错误处理缺位。
- finally 里只清理,绝不 return、绝不 throw;要重抛放在 catch 里。
- 异步回调里抛错先想「栈还在吗」;不在,就让错误走 Promise 链或别抛。
原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| throw 劫持控制流、逐帧拆除 | 7 Errors | text/26-ch07-7-errors.txt:903(搜「hijacks control flow」) · :903(搜「unwound」) |
| 未捕获 → 宿主两种反应 | 同上 | text/26-ch07-7-errors.txt:927(搜「terminate the process」) |
| try-catch 终止传播 | 同上 | text/26-ch07-7-errors.txt:968(搜「reported to the catch block」) |
| catch 位置的张力 | 同上 | text/26-ch07-7-errors.txt:1008(搜「creates a tension」) |
| 语义边界四类 | 同上 | text/26-ch07-7-errors.txt:1014(搜「semantically-meaningful boundaries」) · :1020(搜「Transaction boundaries」) |
| 事务例程 | 同上 | text/26-ch07-7-errors.txt:1041(搜「updateUserProfile」) |
| finally 的吞错与换错 | 同上 | text/26-ch07-7-errors.txt:1080(搜「always runs」) · :1080(搜「suppress or replace」) · :1107(搜「suppressed the error」) |
| finally 是脚枪 | 同上 | text/26-ch07-7-errors.txt:1127(搜「footgun」) |
| 别拿 throw 当流程控制 | 同上 | text/26-ch07-7-errors.txt:1134(搜「Never use throw for control flow」) |
| setTimeout 接不住 | 同上 | text/26-ch07-7-errors.txt:1187(搜「cannot be caught」) |
| 栈已消失 | 同上 | text/26-ch07-7-errors.txt:1216(搜「already completed and vanished」) |
| 恢复与调试两头断 | 同上 | text/26-ch07-7-errors.txt:1228(搜「boundary is severed」) · :1231(搜「no information about what scheduled」) |
| 三行定时器内部栈 | 同上 | text/26-ch07-7-errors.txt:1238(搜「Timeout._onTimeout」) |
| async stack traces 的极限 | 同上 | text/26-ch07-7-errors.txt:1252(搜「async stack traces」) |
| 异步边界总判词 | 同上 | text/26-ch07-7-errors.txt:1258(搜「fundamentally disconnect」) |
| 浏览器全局 error 事件 | 同上 | text/26-ch07-7-errors.txt:1283(搜「addEventListener('error'」) |
| Node uncaughtException | 同上 | text/26-ch07-7-errors.txt:1318(搜「uncaughtException event」) · :1344(搜「considered fatal」) |
| 全局处理器的两大局限 | 同上 | text/26-ch07-7-errors.txt:1350(搜「significant limitations」) · :1356(搜「lost all the local information」) |
| 安全网定调 | 同上 | text/26-ch07-7-errors.txt:1368(搜「safety net」) · :1416(搜「not a primary strategy」) |
| 逐帧拆除与旅程 | 同上 | text/26-ch07-7-errors.txt:903(搜「hijacks control flow」) · :921(搜「chain back」) |
| finally 吞错与换错 | 同上 | text/26-ch07-7-errors.txt:1087(搜「suppressed()」) · :1127(搜「footgun」) |
| throw 当流程控制的禁令 | 同上 | text/26-ch07-7-errors.txt:1134(搜「Never use throw for control flow」) |
| 异步时序与三行裸栈 | 同上 | text/26-ch07-7-errors.txt:1187(搜「cannot be caught」) · :1238(搜「Timeout._onTimeout」) |
| 全局处理器对照 | 同上 | text/26-ch07-7-errors.txt:1283(搜「addEventListener('error'」) · :1325(搜「uncaughtException」) |
| 章末十条总结 | 同上 | text/26-ch07-7-errors.txt:1386(搜「throwing any value」) |
Footnotes
-
出处:「7 Errors」第 903 段(
text/26-ch07-7-errors.txt:903,搜「does not create errors」)。 ↩ -
出处:「7 Errors」第 940–958 段(
text/26-ch07-7-errors.txt:941,搜「Origin point」)。「Never executed」的注释见第 946、951 段。 ↩ -
出处:「7 Errors」第 921 段(
text/26-ch07-7-errors.txt:921,搜「chain back to where execution started」)。 ↩ -
出处:「7 Errors」第 968 段(
text/26-ch07-7-errors.txt:968,搜「propagation up the call stack will stop」)。 ↩ -
出处:「7 Errors」第 1008 段(
text/26-ch07-7-errors.txt:1008,搜「Where you place the catch handler creates a tension」)。 ↩ -
出处:「7 Errors」第 1014 段(
text/26-ch07-7-errors.txt:1014,搜「both enough context」)。 ↩ -
出处:「7 Errors」第 1020–1029 段(
text/26-ch07-7-errors.txt:1020,搜「Transaction boundaries」;四类依次列出)。 ↩ -
出处:「7 Errors」第 1041–1052 段(
text/26-ch07-7-errors.txt:1049,搜「tx.rollback()」;:1050搜「Profile update failed」)。「为什么不在单条查询处 catch」的解释见第 1034 段(搜「too early」)。 ↩ -
出处:「7 Errors」第 1080 段(
text/26-ch07-7-errors.txt:1080,搜「regardless of whether an error was thrown」)。 ↩ -
出处:「7 Errors」第 1080 段(
text/26-ch07-7-errors.txt:1080,搜「completely suppress or replace the error」)。 ↩ -
出处:「7 Errors」第 1121 段(
text/26-ch07-7-errors.txt:1121,搜「recovery or debugging is lost」)。 ↩ -
出处:「7 Errors」第 1127 段(
text/26-ch07-7-errors.txt:1127,搜「footgun」)。 ↩ -
出处:「7 Errors」第 1134 段(
text/26-ch07-7-errors.txt:1134,搜「Never use throw for control flow」)。 ↩ -
出处:「7 Errors」第 1140–1155 段(
text/26-ch07-7-errors.txt:1150,搜「throw item」)。 ↩ -
出处:「7 Errors」第 1167 段(
text/26-ch07-7-errors.txt:1167,搜「appropriate keywords like return」)。 ↩ -
出处:「7 Errors」第 1194–1204 段(
text/26-ch07-7-errors.txt:1196,搜「Processing failed」)。 ↩ -
出处:「7 Errors」第 1216 段(
text/26-ch07-7-errors.txt:1216,搜「already completed and vanished」)。 ↩ -
出处:「7 Errors」第 1222–1246 段(
text/26-ch07-7-errors.txt:1228,搜「boundary is severed」;:1231搜「what scheduled it or why」)。 ↩ -
出处:「7 Errors」第 1237–1241 段(
text/26-ch07-7-errors.txt:1239,搜「listOnTimeout」)。 ↩ -
出处:「7 Errors」第 1246 段(
text/26-ch07-7-errors.txt:1246,搜「nothing about processData」)。 ↩ -
出处:「7 Errors」第 1252 段(
text/26-ch07-7-errors.txt:1252,搜「incapable of helping with the setTimeout」)。 ↩ -
出处:「7 Errors」第 1258 段(
text/26-ch07-7-errors.txt:1258,搜「fundamentally disconnect the error from its origin」)。「越境点清单」在同段(setTimeout、nextTick、事件监听、Promise 回调)。 ↩ -
出处:「7 Errors」第 1270 段(
text/26-ch07-7-errors.txt:1270,搜「absolute last line of defense」)。 ↩ -
出处:「7 Errors」第 1276–1302 段(
text/26-ch07-7-errors.txt:1284,搜「event.error」;:1287搜「preventDefault」)。 ↩ -
出处:「7 Errors」第 1344 段(
text/26-ch07-7-errors.txt:1344,搜「considered fatal」)。 ↩ -
出处:「7 Errors」第 1350–1362 段(
text/26-ch07-7-errors.txt:1350,搜「significant limitations」)。 ↩ -
出处:「7 Errors」第 1362 段(
text/26-ch07-7-errors.txt:1362,搜「logged, not where they go to be understood」)。 ↩ -
出处:「7 Errors」第 1368 段(
text/26-ch07-7-errors.txt:1368,搜「a signal that error handling at proper boundaries is missing」)。 ↩ -
出处:「7 Errors」第 915–958 段(
text/26-ch07-7-errors.txt:921,搜「chain back to where execution started」):每帧代表一次函数调用,throw 沿链上卷直到处理器或栈空。 ↩ -
出处:「7 Errors」第 903 段(
text/26-ch07-7-errors.txt:903,搜「hijacks control flow」):「throw 不创建错误,它劫持控制流……每一层调用都被逐层拆除。」异常路径的额外成本论证里用了第 813 段(每个 Error 数千字节)与拆除语义。 ↩ -
出处:「7 Errors」第 968–998 段(
text/26-ch07-7-errors.txt:968,搜「propagation up the call stack will stop」):接住后「传播停止」;「中间层状态灰飞烟灭」是逐帧拆除语义的直接推论(见第 903 段)。 ↩ -
出处:「7 Errors」第 1014–1029 段(
text/26-ch07-7-errors.txt:1020,搜「Transaction boundaries」;四类依次列出)。各边界的判据是我们的提炼,依据是同段「既有恢复决策的上下文、又能为上层保留调试细节」的定义。 ↩ -
出处:「7 Errors」第 1008 段(
text/26-ch07-7-errors.txt:1008,搜「creates a tension」)与第 1034 段(搜「too early」):深处缺恢复信息、高处丢调试细节,事务处刚刚好。 ↩ -
出处:「7 Errors」第 1041–1052 段(
text/26-ch07-7-errors.txt:1049,搜「tx.rollback()」;:1050搜「Profile update failed」)。 ↩ -
出处:「7 Errors」第 1087–1105 段(
text/26-ch07-7-errors.txt:1087,搜「suppressed()」;:1109搜「Success!」;:1111搜「This replaces it」)。 ↩ -
出处:「7 Errors」第 1121 段(
text/26-ch07-7-errors.txt:1121,搜「recovery or debugging is lost」)。 ↩ -
出处:「7 Errors」第 1127 段(
text/26-ch07-7-errors.txt:1127,搜「footgun」):「这是规范有意设计的、给开发者的最大控制权——但更多时候,它是对不小心的开发者的脚枪。」 ↩ -
出处:「7 Errors」第 1134–1155 段(
text/26-ch07-7-errors.txt:1134,搜「Never use throw for control flow」;反模式代码见第 1147 段)。 ↩ -
出处:「7 Errors」第 1161 段(
text/26-ch07-7-errors.txt:1161,搜「absolutely kill the performance」):「以前这种写法会彻底杀死性能,因为 V8 优化不了 try-catch;现在不再有这个 问题,但它仍导致性能与调试问题,以及难以维护。」 ↩ -
出处:「7 Errors」第 1167 段(
text/26-ch07-7-errors.txt:1167,搜「appropriate keywords like return」):「用 return、break、continue 做流程控制;throw/try/catch 只留给错误。」 ↩ -
出处:「7 Errors」第 1237–1246 段(
text/26-ch07-7-errors.txt:1238,搜「Timeout._onTimeout」;:1246搜「nothing about processData」)。 ↩ -
出处:「7 Errors」第 1318–1344 段(
text/26-ch07-7-errors.txt:1325,搜「process.on('uncaughtException'」;:1344搜「considered fatal」)。 ↩ -
出处:「7 Errors」第 1350–1356 段(
text/26-ch07-7-errors.txt:1356,搜「lost all the local information」):「等错误到了全局处理器,你已经丢了做恢复决策所需的全部本地信息。」 ↩ -
出处:「7 Errors」第 1362 段(
text/26-ch07-7-errors.txt:1362,搜「logged, not where they go to be understood」)。 ↩ -
出处:「7 Errors」第 1368 段(
text/26-ch07-7-errors.txt:1368,搜「safety net」)与第 1416 段(搜「not a primary strategy」):「错误经常到达全局处理器,信号是边界的错误处理缺位了。」 ↩ -
出处:「7 Errors」第 1380–1416 段(
text/26-ch07-7-errors.txt:1386,搜「throwing any value」;十条依次列出,编号是我们的整理)。 ↩ -
出处:「7 Errors」第 1216 段(
text/26-ch07-7-errors.txt:1216,搜「already completed and vanished」):「等回调真的执行时,原来包含 try-catch 的调用栈早已完成、消失。」 ↩ -
出处:「7 Errors」第 1258 段(
text/26-ch07-7-errors.txt:1258,搜「fundamentally disconnect」):「异步边界从根本上把错误和它的来源拆开……setTimeout、process.nextTick、事件监听、Promise 回调,所有把工作排到晚点的 API 都跨过这条线。」 ↩ -
出处:「1 When a hello isn’t hello」第 664 段(
text/04-ch01-1-when-a-hello-isn-t-hello.txt:664,搜「not directly related」):「微任务队列和事件循环根本没有直接关系。」 ↩ -
出处:「7 Errors」第 940–958 段、第 1194–1204 段(
text/26-ch07-7-errors.txt:946,搜「Never executed」)。 ↩ -
出处:「7 Errors」第 1344 段(
text/26-ch07-7-errors.txt:1344,搜「should not continue normal operation」)。 ↩