第54课:错误处理与调试——try-catch-finally、自定义错误、debugger、Source Map
程序运行过程中不可避免地会发生错误。JavaScript 提供了 try-catch-finally 语句来捕获和处理运行时错误,避免整个程序崩溃。对于开发者,浏览器和 Node.js 提供了 debugger 语句和开发者工具来定位和修复代码缺陷。而 Source Map 则是连接压缩后生产代码与原始源代码的桥梁,使线上调试成为可能。本节课将系统讲解错误类型、异常捕获语句的完整语法、自定义错误类的设计模式,以及浏览器调试工具的核心用法和 Source Map 的生成原理。
1. JavaScript 中的错误类型
1.1 内置错误构造函数
JavaScript 原生提供了七种错误类型,它们都继承自 Error。
| 构造函数 | 触发场景 | 示例 |
|---|---|---|
Error |
通用错误。 | throw new Error('Something went wrong') |
SyntaxError |
代码语法错误(解析阶段抛出,通常无法被 try-catch 捕获)。 |
eval('foo bar') |
ReferenceError |
引用不存在的变量。 | console.log(undefinedVar) |
TypeError |
值的类型不符合预期。 | null.f()、42.toUpperCase() |
RangeError |
值超出有效范围。 | new Array(-1)、(1.23).toFixed(101) |
URIError |
encodeURI 或 decodeURI 传入无效参数。 |
decodeURI('%') |
EvalError |
eval 相关错误(现代 JS 已极少抛出)。 |
已几乎不再使用。 |
1 | try { |
注意:代码解析阶段(在脚本加载时)的 SyntaxError 发生在编译阶段,此时 try-catch 尚未执行,因此无法捕获。只有运行时的语法错误(如 eval 内部的字符串解析)才能被捕获。
1.2 Error 实例的核心属性
| 属性 | 说明 |
|---|---|
message |
错误描述信息。 |
name |
错误类型的名称(如 'TypeError'、'Error')。 |
stack |
错误发生时的调用栈字符串(非标准属性,但所有现代浏览器和 Node.js 都支持)。 |
cause |
ES2022 引入:可在 new Error(msg, { cause: originalError }) 中指定原始错误,形成错误链。 |
1 | function readFile(path) { |
2. try-catch-finally 完整语法
2.1 基本结构
1 | try { |
catch 的参数可省略(ES2019):如果不需要访问错误对象,可以写 catch {}。
2.2 finally 的执行时机
finally 块在 try 或 catch 执行完毕后始终运行,即使 try 或 catch 中有 return 语句。finally 在 return 语句生效之前执行,且 finally 中的 return 会覆盖 try/catch 的返回值。
1 | function test() { |
最佳实践:finally 中**避免使用 return**,它会使函数行为难以理解。finally 应仅用于清理资源(如关闭文件、清除定时器、恢复状态)。
2.3 catch 的条件捕获(非标准,但常见于某些引擎)
JavaScript 原生不支持带条件的 catch(如 catch (e if e instanceof TypeError))。需要条件判断时,在 catch 内部做类型检查并重新抛出:
1 | try { |
2.4 try-catch 的性能代价
try 块中的代码在运行时会被优化器限制(因为需要保持异常路径的正确性)。不应将 try-catch 用于控制流,它仅应用于真正的异常情况。将整个应用逻辑包裹在 try-catch 中不是好的实践——应只包裹可能抛出错误的特定操作。
3. throw 与自定义错误
3.1 throw 可以抛出任意值
在 JavaScript 中,throw 可以抛出任何类型的值(字符串、数字、对象),但最佳实践是始终抛出 Error 或其子类的实例。因为只有 Error 实例才会自动包含调用栈信息(stack 属性)。
1 | // ❌ 抛字符串:没有 stack,无法追踪来源 |
3.2 自定义错误类
扩展 Error 创建业务领域特定的错误类型,使错误处理更精确。
1 | class ValidationError extends Error { |
3.3 错误包装与错误链
在处理底层错误时,通常用更高级别的错误包装原始错误,并保留原始错误上下文(使用 cause 属性)。
1 | async function fetchUserData(userId) { |
ES2022 的 cause 属性在 Node.js 16.9+ 和现代浏览器中可用。在错误日志中可递归打印 cause 链来追踪根本原因。
4. debugger 语句与开发者工具调试
4.1 debugger 语句
在代码中插入 debugger; 语句,当浏览器开发者工具打开时,代码执行会在该语句处暂停,进入调试模式。如果开发者工具未打开,该语句不会产生任何效果。
1 | function calculate(a, b) { |
注意:永远不要将 debugger 语句提交到生产代码。可以使用 ESLint 的 no-debugger 规则在 CI/CD 中阻止提交。
4.2 浏览器开发者工具的调试能力
| 功能 | 说明 | 快捷键(Chrome) |
|---|---|---|
| 断点 | 在 Sources 面板中点击行号设置断点,代码执行到该行暂停。 | — |
| 条件断点 | 右键行号 → 添加条件断点,仅当条件为 true 时暂停。 | — |
| DOM 断点 | 右键 DOM 元素 → Break on → 子树修改/属性修改/节点移除。 | — |
| XHR 断点 | Sources → XHR Breakpoints,在发送 Ajax 请求时暂停。 | — |
| Step Over | 执行当前行,不进入函数内部。 | F10 |
| Step Into | 进入函数内部。 | F11 |
| Step Out | 跳出当前函数。 | Shift + F11 |
| Watch 表达式 | 实时监视自定义表达式的值。 | — |
| Call Stack | 查看当前调用栈,点击可跳转到对应函数。 | — |
| Scope | 查看当前作用域内的局部变量、闭包变量和全局变量。 | — |
实用技巧:
- 在
console.log中使用对象的大括号包裹变量名可输出更清晰的日志:console.log({ userId, userName })。 console.table可用于数组和对象的表格化展示。console.time(label)和console.timeEnd(label)可测量代码执行耗时。
5. Source Map:生产代码的调试桥梁
5.1 什么是 Source Map?
生产环境的 JavaScript 代码通常经过压缩(Minify)和打包(Bundle),变量名被缩短、空白被移除、多文件合并为一个。这使得调试变得极其困难——错误堆栈中显示的行号和列号对应的是压缩后的代码,无法定位原始源码。
Source Map 是一个 JSON 文件,记录了压缩后代码的每个位置与原始源码位置之间的映射关系。浏览器开发者工具加载 Source Map 后,可以在调试器中直接显示原始源码,在原始代码上设置断点,查看原始变量名。
5.2 Source Map 的结构
1 | { |
关键字段:
sources:原始源文件路径数组。sourcesContent(可选):原始源文件的完整内容(嵌入后无需额外请求源文件)。mappings:VLQ 编码的映射字符串,将压缩代码的每个 Token 映射到原始代码的行/列位置。
5.3 如何生成 Source Map
主流构建工具都内置了 Source Map 生成能力:
Vite / Webpack:配置 devtool 选项。
1 | // vite.config.js |
常用 devtool 值对比:
| 配置值 | 生成速度 | 精确度 | 说明 |
|---|---|---|---|
eval |
最快 | 行级 | 仅开发环境用,不生成独立文件。 |
eval-source-map |
较快 | 列级 | 开发环境推荐,包含列信息,内嵌在 eval 中。 |
source-map |
最慢 | 列级 | 生产环境推荐,生成独立 .map 文件。 |
hidden-source-map |
最慢 | 列级 | 生成 .map 但不添加引用注释(仅用于错误追踪平台)。 |
nosources-source-map |
最慢 | 列级 | 不包含 sourcesContent(保护源码隐私)。 |
5.4 Source Map 的安全考量
不要在公开生产环境暴露 Source Map(除非你希望任何人查看原始源码)。可以通过以下策略管理:
- 将
.map文件上传到错误追踪服务(如 Sentry),但不部署到公开服务器。 - 使用
hidden-source-map:生成 map 但不添加引用注释,仅内部错误追踪工具使用。 - 仅在受 IP 限制的内部环境或通过认证访问
.map文件。 - 使用
nosources-source-map隐藏源码内容。
6. 全局错误捕获
6.1 浏览器环境:window.onerror 与 unhandledrejection
1 | // 捕获全局未处理的同步错误(以及部分资源加载错误) |
6.2 Node.js 环境
1 | process.on('uncaughtException', (error) => { |
注意:全局错误捕获是最后一道防线,不应依赖它来处理业务逻辑。它主要用于记录日志和优雅降级。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 以下哪种错误类型会在访问
null.property时抛出?
A.SyntaxError
B.ReferenceError
C.TypeError
D.RangeError(单选) 关于
finally块的说法,正确的是?
A.finally仅在try成功时执行。
B.finally中的return不会影响try的返回值。
C.finally在try或catch执行完毕后始终运行,即使有return。
D.finally块是可选的,没有意义。(填空) 要创建自定义错误类,应使用
______关键字继承Error,并在构造函数中设置this.name以标识错误类型。(多选) 以下哪些工具或方法可用于调试 JavaScript 代码?
A.debugger语句
B. 浏览器 Sources 面板的条件断点
C.console.log
D. Source Map
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 JavaScript 函数 safeJsonParse(str, fallback),它接收一个 JSON 字符串和一个回退值。如果解析成功,返回解析后的对象;如果解析失败(抛出异常),返回 fallback 值。要求如下:
- 使用
try-catch捕获JSON.parse可能抛出的异常。 - 在
catch块中,检查错误是否为SyntaxError(因为JSON.parse对无效 JSON 抛出SyntaxError),并记录一条警告到控制台(console.warn),包含原始错误消息。 - 如果是其他类型错误,将其重新抛出(不做处理)。
- 使用
const声明函数,添加 JSDoc 注释说明参数、返回值和错误处理策略。 - 提供至少 3 个测试用例(正常 JSON、无效 JSON、有效但非 JSON 格式的字符串)。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 try-catch 的结构、错误类型判断、以及重新抛出的逻辑。
三、面试真题与参考答案
题目(阿里巴巴前端面试题):
请详细说明 JavaScript 中
try-catch的工作原理和最佳实践。如果一个异步函数(返回 Promise)内部抛出了错误,外层的try-catch能否捕获?为什么?应该如何处理?
参考答案:
1. try-catch 工作原理:try 块中的代码被监视执行。一旦抛出异常,引擎立即跳转到 catch 块,并传入错误对象。finally 块无论是否发生异常都会执行。try-catch 仅能捕获同步执行中抛出的错误——即调用栈中直接抛出的异常。它不能捕获异步操作(如 setTimeout 回调、Promise 链、事件监听器)中抛出的错误,因为这些错误发生在新的调用栈中,此时 try-catch 上下文已经退出。
2. 异步错误无法被外层 try-catch 捕获:
1 | try { |
3. 正确的异步错误处理方式:
- Promise 内部:使用
.catch()方法或async/await配合try-catch(将await放在try块内)。 - 回调模式:回调函数内部自行
try-catch处理。 - 全局兜底:监听
unhandledrejection事件(浏览器)或process.on('unhandledRejection')(Node.js)捕获未被任何.catch处理的 Promise 拒绝。
1 | // async/await 正确捕获 |
课后练习答案
一、概念自测答案
C
- 解析:在
null上访问属性时 JavaScript 抛出TypeError,因为操作试图在原始值上访问属性。
- 解析:在
C
- 解析:
finally始终执行,即使try或catch中有return。但finally中的return会覆盖try/catch的返回值。
- 解析:
class ... extends- 解析:通过
class MyError extends Error { constructor(...) { super(...); this.name = 'MyError'; } }创建自定义错误。
- 解析:通过
A、B、C、D
- 解析:
debugger语句、条件断点、console.log日志、Source Map 都是 JavaScript 调试工具或方法。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数safeJsonParse(str, fallback),安全地解析 JSON 字符串。要求:
- 使用
const声明函数,添加 JSDoc 注释。- 在
try块中调用JSON.parse(str),解析成功则返回结果。- 在
catch (error)块中判断error instanceof SyntaxError,若是则console.warn('JSON 解析失败:', error.message)并返回fallback。- 若错误不是
SyntaxError,则throw error重新抛出。- 提供测试用例注释:
safeJsonParse('{"a":1}', {})→{ a: 1 }safeJsonParse('{invalid}', null)→null(并输出警告)safeJsonParse('"just a string"', [])→'just a string'(合法 JSON)- 直接输出完整代码。”