跳转至

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 节给了最小示例,这里把「逐帧拆除」走成慢动作31:

  栈(自下而上):        [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 节)既难看又真贵:它在为「正常的返回路径」付「异常的拆除费」32。

catch 块的位置决定了拆除在哪一层停。同一个错误,被 middle 接住与被 outer 接住,对系统的意义完全不同:middle 接住,middle 以上的层 (包括 outer 的 try)都感知不到任何事;outer 接住, 中间所有层的本地状态已经全部灰飞烟灭——这就是为什么「放哪一层」 是个有实质后果的决策,而不是风格问题33。

3.2b 四类边界的完整定义与判据

3.2 节列了四类语义边界,这里给每类一个「为什么是它」的判据34:

边界 判据:为什么恢复决策在这里才合法
事务边界 提交/回滚是针对「整组操作」的语义,单条查询失败时无法决定——必须等整组收尾
API 边界 重试/降级/熔断的依据是「这次外部调用」的合约(幂等吗?有备用端点吗?),只有调用方知道
用户操作边界 「告诉用户失败了、保留他已填的表单」需要 UI 上下文,深处没有
资源边界 释放/重试的决策需要「这个资源现在归谁管」的知识,获取处最清楚

反过来,两个反例也成立:在单条查询处 catch——你不知道该不该回滚, 因为组里还有三条没跑;在应用顶层 catch——你知道该「记日志、 返回 500」,可「具体哪一步、为什么失败」已经在四层拆除里磨没了35。 事务例程里那个 throw new Error('Profile update failed', { cause: e }) 的写法,是「边界上抛干净的 + 链条上保完整的」的标准动作: 外层拿到一句人话,内层的原始错误躺在 cause 里跟着走36。

3.3b finally 的全规则与两个反例的展开

finally 的规则比「总是执行」多一条关键的:它能劫持传播中的错误。 原书的两个反例,展开看状态37:

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 截胡,一切白搭,恢复与调试两头的信息全部作废38。 书里给这种行为的定性是:规范有意给开发者的最大控制权, 本意是让资源清理不被异常打断;可对不小心的人,它是上了膛的脚枪39。 纪律因此只有一条: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 当流程控制」的完整论证

原书把这条禁令写成了带历史背景的论证,值得照录40。反模式长这样:

function findItem(array, test) {
  try {
    array.forEach((item) => {
      if (test(item)) throw item;    // 把 throw 当 break 使
    });
  } catch (result) {
    return result;
  }
}

「找到就抛、外层接住当返回值」——把异常机制当 goto。 历史上这招是真性能的灾难:早期的 V8 优化不了 try-catch, 这种写法的性能可以直接杀死应用;如今引擎能优化了, 它仍然是烂代码:可读性、可调试性、维护性全线崩塌41。 规则书里写得很死:该用 return/break/continue 的地方就用它们; throw/try/catch 只留给真正的异常42。 一句话版本:异常是「计划外的出口」,不是「方便的跳转」。

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 排的队、排它是为了处理什么数据」43。 恢复侧与调试侧在这张图上是同一场塌方:错误的「来源上下文」 (谁排的、为什么排)随旧栈一起死了,剩下的只有一个裸错误 掉在全新的世界里——它能落进的地方只有全局处理器。

3.6b 全局处理器的能力边界:一张对照表

浏览器 window.addEventListener('error', ...) Node.js process.on('uncaughtException', ...)
能接到 同步 + 异步的所有漏网错误 同步 + 异步的所有漏网错误
拿到什么 错误对象、文件名、行号;preventDefault() 可阻止默认报错 错误对象 + origin(同步/异步来源)
接完默认行为 控制台报错(可被 preventDefault 阻止) 没有处理器才退出;有了处理器进程继续,但状态已未定义
定位 兜底日志/监控的最后一站 同左;Node 官方定性为「致命」,处理完应尽快退出44

两个用途都是真的:记日志、报监控。但两个局限同样硬—— 对恢复:全局处理器离现场太远,错误到这时,不知道当时在做什么操作、 涉及什么数据、系统处于什么状态,能做的只有记录加祈祷45; 对调试:同步错误的栈虽完整,可已经穿过不知多少层抽象; 异步错误的栈照旧残缺。书里那句定性值得背: 「全局处理器是错误被记录的地方,不是错误被理解的地方。」46

所以最终定调只有一句:真正的解法是在语义边界上接住错误, 别让它们走到这一步;全局处理器是安全网,用来兜「错误处理本身的失误」。 错误经常落到全局处理器,信号很明确——边界的错误处理缺位了47。

3.7 本章的章末总结(原书十条)

原书第 7 章结尾给了十条总结,照录作核对单48:

  1. 可以 throw 任何值,但带栈的 Error 对象是恢复与调试两用的约定俗成;
  2. 原生类型(TypeError/RangeError)描述语言违规,不给恢复指导; AggregateError 与 DOMException 的语义类型更实用;
  3. cause 支持边界处包错误:对外干净、对内保完整链;
  4. Node.js 错误码把机器恢复与人读消息分开,告别了脆弱的「匹配消息」;
  5. 调用栈为调试抓现场,代价是实打实的内存与 CPU;惰性序列化与限深度是对策;
  6. try-catch 的传播是逐帧拆除,直到找到处理器或撞毁;
  7. catch 放语义边界:事务、API、用户操作、资源;
  8. finally 保证清理,但 return/throw 会吞错换错;
  9. setTimeout 等异步操作拆掉调用栈,try-catch 失效,栈丢失调度上下文;
  10. 全局处理器是兜底网;错误常落到那里 = 边界处理缺位。

十条里有六条是「不要做什么」——错误处理这个主题,禁令比技法多。 这倒符合它的本质:错误路径是平时走不到、出事时救命的路, 它的每一处分岔都要在出事前就定好。 而定分岔的依据,就是 「恢复要上下文、调试要现场、两者成本对立」这条主线。

3.7b 与第 1 章的闭环:队列里的错误没人接

把第 1 章的队列图和本章的旅程图叠在一起,一个完整的画面就出来了: 错误在同一次调用栈里 throw 时,try-catch 是有效的——它们共享同一摞帧; 错误在另一个任务里 throw 时,两个任务的栈中间隔着整个事件循环, try-catch 物理上够不着49。所以「异步边界断裂」不是错误系统的缺陷, 而是调度系统的必然:01 章学的每一个队列(microtask、nextTick、 immediates、定时器),都是一道栈的断层线;错误每跨过一条, 就要么靠 Promise 链那套「平行的栈」接力,要么掉进全局处理器50。

这条闭环也解释了第 1 章那个反常的断言——「微任务队列和事件循环 没有直接关系」51:微任务是语言定义的队列,宿主想什么时候清就什么时候清; 而错误传播恰恰依赖「同一摞帧」,宿主对队列的自由裁量, 等于对错误传播路径的自由裁量。调度的自由,就是错误处理的代价—— 同一个硬币的两面,被这本书拆在两章里讲完。

作者的判断与证据

有证据的判断:

  • try-catch/finally/setTimeout 的行为演示都是可运行的代码, 「Never executed」「Never catches it」这些输出都可直接复现29;
  • 「Node 视未捕获为致命」是 Node 文档明载的行为30。

作者的主张:

  • 「catch 放语义边界」是本 书的核心实践主张之一; 「四类边界」清单是作者经验归纳,不是规范分类。
  • 「全局处理器只配当安全网」是立场鲜明的定调; 现实中(尤其在短生命周期脚本、CLI 工具里)把 uncaughtException 当主策略用的代码不少,书里没讨论这些场景的宽容度。

边界与局限

  • Promise/async 的错误传播(本书多次预告的「第 8 章」)在 MEAP 里 不存在——异步错误机制的正确答案这一章故意只开了个头; async/await 如何用 try-catch 找回同步感,要等最终版。
  • async stack traces 的具体能力(V8 的 --async-stack-traces 及默认行为) 书中只给了一段落,细节随版本变化。
  • 浏览器 unhandledrejection 事件(Promise 专属兜底)全书未提—— 它是和 uncaughtException 平行的另一半兜底体系。
  • Worker/跨线程的错误传播(postMessage 传不了 Error 等)不在范围。

可带走的

  1. throw 劫持控制流、沿栈逐帧拆除;catch 在哪,拆除就停在哪。
  2. catch 放深了下不了恢复决策,放高了丢调试细节——放语义边界上。
  3. 四类边界:事务、API、用户操作、资源;事务示例的模式(rollback + cause 重抛)直接可抄。
  4. finally 永远执行,但 return/throw 会吞掉或替换原错误;finally 只清理。
  5. throw 不是流程控制;return/break/continue 干它们该干的事。
  6. setTimeout 等异步回调里抛错,外层 try-catch 永远接不住——栈已经没了。
  7. async stack traces 补调试不补恢复,且对 Web API 调度完全无效。
  8. 全局处理器(浏览器 error 事件 / Node uncaughtException)只配当安全网; 错误常落到这里 = 边界错误处理缺位。
  9. finally 里只清理,绝不 return、绝不 throw;要重抛放在 catch 里。
  10. 异步回调里抛错先想「栈还在吗」;不在,就让错误走 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」)


  1. 出处:「7 Errors」第 903 段(text/26-ch07-7-errors.txt:903,搜「does not create errors」)。 ↩

  2. 出处:「7 Errors」第 940–958 段(text/26-ch07-7-errors.txt:941,搜「Origin point」)。「Never executed」的注释见第 946、951 段。 ↩

  3. 出处:「7 Errors」第 921 段(text/26-ch07-7-errors.txt:921,搜「chain back to where execution started」)。 ↩

  4. 出处:「7 Errors」第 968 段(text/26-ch07-7-errors.txt:968,搜「propagation up the call stack will stop」)。 ↩

  5. 出处:「7 Errors」第 1008 段(text/26-ch07-7-errors.txt:1008,搜「Where you place the catch handler creates a tension」)。 ↩

  6. 出处:「7 Errors」第 1014 段(text/26-ch07-7-errors.txt:1014,搜「both enough context」)。 ↩

  7. 出处:「7 Errors」第 1020–1029 段(text/26-ch07-7-errors.txt:1020,搜「Transaction boundaries」;四类依次列出)。 ↩

  8. 出处:「7 Errors」第 1041–1052 段(text/26-ch07-7-errors.txt:1049,搜「tx.rollback()」;:1050 搜「Profile update failed」)。「为什么不在单条查询处 catch」的解释见第 1034 段(搜「too early」)。 ↩

  9. 出处:「7 Errors」第 1080 段(text/26-ch07-7-errors.txt:1080,搜「regardless of whether an error was thrown」)。 ↩

  10. 出处:「7 Errors」第 1080 段(text/26-ch07-7-errors.txt:1080,搜「completely suppress or replace the error」)。 ↩

  11. 出处:「7 Errors」第 1121 段(text/26-ch07-7-errors.txt:1121,搜「recovery or debugging is lost」)。 ↩

  12. 出处:「7 Errors」第 1127 段(text/26-ch07-7-errors.txt:1127,搜「footgun」)。 ↩

  13. 出处:「7 Errors」第 1134 段(text/26-ch07-7-errors.txt:1134,搜「Never use throw for control flow」)。 ↩

  14. 出处:「7 Errors」第 1140–1155 段(text/26-ch07-7-errors.txt:1150,搜「throw item」)。 ↩

  15. 出处:「7 Errors」第 1167 段(text/26-ch07-7-errors.txt:1167,搜「appropriate keywords like return」)。 ↩

  16. 出处:「7 Errors」第 1194–1204 段(text/26-ch07-7-errors.txt:1196,搜「Processing failed」)。 ↩

  17. 出处:「7 Errors」第 1216 段(text/26-ch07-7-errors.txt:1216,搜「already completed and vanished」)。 ↩

  18. 出处:「7 Errors」第 1222–1246 段(text/26-ch07-7-errors.txt:1228,搜「boundary is severed」;:1231 搜「what scheduled it or why」)。 ↩

  19. 出处:「7 Errors」第 1237–1241 段(text/26-ch07-7-errors.txt:1239,搜「listOnTimeout」)。 ↩

  20. 出处:「7 Errors」第 1246 段(text/26-ch07-7-errors.txt:1246,搜「nothing about processData」)。 ↩

  21. 出处:「7 Errors」第 1252 段(text/26-ch07-7-errors.txt:1252,搜「incapable of helping with the setTimeout」)。 ↩

  22. 出处:「7 Errors」第 1258 段(text/26-ch07-7-errors.txt:1258,搜「fundamentally disconnect the error from its origin」)。「越境点清单」在同段(setTimeout、nextTick、事件监听、Promise 回调)。 ↩

  23. 出处:「7 Errors」第 1270 段(text/26-ch07-7-errors.txt:1270,搜「absolute last line of defense」)。 ↩

  24. 出处:「7 Errors」第 1276–1302 段(text/26-ch07-7-errors.txt:1284,搜「event.error」;:1287 搜「preventDefault」)。 ↩

  25. 出处:「7 Errors」第 1344 段(text/26-ch07-7-errors.txt:1344,搜「considered fatal」)。 ↩

  26. 出处:「7 Errors」第 1350–1362 段(text/26-ch07-7-errors.txt:1350,搜「significant limitations」)。 ↩

  27. 出处:「7 Errors」第 1362 段(text/26-ch07-7-errors.txt:1362,搜「logged, not where they go to be understood」)。 ↩

  28. 出处:「7 Errors」第 1368 段(text/26-ch07-7-errors.txt:1368,搜「a signal that error handling at proper boundaries is missing」)。 ↩

  29. 出处:「7 Errors」第 940–958 段、第 1194–1204 段(text/26-ch07-7-errors.txt:946,搜「Never executed」)。 ↩

  30. 出处:「7 Errors」第 1344 段(text/26-ch07-7-errors.txt:1344,搜「should not continue normal operation」)。 ↩

  31. 出处:「7 Errors」第 915–958 段(text/26-ch07-7-errors.txt:921,搜「chain back to where execution started」):每帧代表一次函数调用,throw 沿链上卷直到处理器或栈空。 ↩

  32. 出处:「7 Errors」第 903 段(text/26-ch07-7-errors.txt:903,搜「hijacks control flow」):「throw 不创建错误,它劫持控制流……每一层调用都被逐层拆除。」异常路径的额外成本论证里用了第 813 段(每个 Error 数千字节)与拆除语义。 ↩

  33. 出处:「7 Errors」第 968–998 段(text/26-ch07-7-errors.txt:968,搜「propagation up the call stack will stop」):接住后「传播停止」;「中间层状态灰飞烟灭」是逐帧拆除语义的直接推论(见第 903 段)。 ↩

  34. 出处:「7 Errors」第 1014–1029 段(text/26-ch07-7-errors.txt:1020,搜「Transaction boundaries」;四类依次列出)。各边界的判据是我们的提炼,依据是同段「既有恢复决策的上下文、又能为上层保留调试细节」的定义。 ↩

  35. 出处:「7 Errors」第 1008 段(text/26-ch07-7-errors.txt:1008,搜「creates a tension」)与第 1034 段(搜「too early」):深处缺恢复信息、高处丢调试细节,事务处刚刚好。 ↩

  36. 出处:「7 Errors」第 1041–1052 段(text/26-ch07-7-errors.txt:1049,搜「tx.rollback()」;:1050 搜「Profile update failed」)。 ↩

  37. 出处:「7 Errors」第 1087–1105 段(text/26-ch07-7-errors.txt:1087,搜「suppressed()」;:1109 搜「Success!」;:1111 搜「This replaces it」)。 ↩

  38. 出处:「7 Errors」第 1121 段(text/26-ch07-7-errors.txt:1121,搜「recovery or debugging is lost」)。 ↩

  39. 出处:「7 Errors」第 1127 段(text/26-ch07-7-errors.txt:1127,搜「footgun」):「这是规范有意设计的、给开发者的最大控制权——但更多时候,它是对不小心的开发者的脚枪。」 ↩

  40. 出处:「7 Errors」第 1134–1155 段(text/26-ch07-7-errors.txt:1134,搜「Never use throw for control flow」;反模式代码见第 1147 段)。 ↩

  41. 出处:「7 Errors」第 1161 段(text/26-ch07-7-errors.txt:1161,搜「absolutely kill the performance」):「以前这种写法会彻底杀死性能,因为 V8 优化不了 try-catch;现在不再有这个 问题,但它仍导致性能与调试问题,以及难以维护。」 ↩

  42. 出处:「7 Errors」第 1167 段(text/26-ch07-7-errors.txt:1167,搜「appropriate keywords like return」):「用 return、break、continue 做流程控制;throw/try/catch 只留给错误。」 ↩

  43. 出处:「7 Errors」第 1237–1246 段(text/26-ch07-7-errors.txt:1238,搜「Timeout._onTimeout」;:1246 搜「nothing about processData」)。 ↩

  44. 出处:「7 Errors」第 1318–1344 段(text/26-ch07-7-errors.txt:1325,搜「process.on('uncaughtException'」;:1344 搜「considered fatal」)。 ↩

  45. 出处:「7 Errors」第 1350–1356 段(text/26-ch07-7-errors.txt:1356,搜「lost all the local information」):「等错误到了全局处理器,你已经丢了做恢复决策所需的全部本地信息。」 ↩

  46. 出处:「7 Errors」第 1362 段(text/26-ch07-7-errors.txt:1362,搜「logged, not where they go to be understood」)。 ↩

  47. 出处:「7 Errors」第 1368 段(text/26-ch07-7-errors.txt:1368,搜「safety net」)与第 1416 段(搜「not a primary strategy」):「错误经常到达全局处理器,信号是边界的错误处理缺位了。」 ↩

  48. 出处:「7 Errors」第 1380–1416 段(text/26-ch07-7-errors.txt:1386,搜「throwing any value」;十条依次列出,编号是我们的整理)。 ↩

  49. 出处:「7 Errors」第 1216 段(text/26-ch07-7-errors.txt:1216,搜「already completed and vanished」):「等回调真的执行时,原来包含 try-catch 的调用栈早已完成、消失。」 ↩

  50. 出处:「7 Errors」第 1258 段(text/26-ch07-7-errors.txt:1258,搜「fundamentally disconnect」):「异步边界从根本上把错误和它的来源拆开……setTimeout、process.nextTick、事件监听、Promise 回调,所有把工作排到晚点的 API 都跨过这条线。」 ↩

  51. 出处:「1 When a hello isn’t hello」第 664 段(text/04-ch01-1-when-a-hello-isn-t-hello.txt:664,搜「not directly related」):「微任务队列和事件循环根本没有直接关系。」 ↩