WinddSnow

JavaScript-Error-Handling-and-Debugging

字数统计: 4.1k阅读时长: 16 min
2026/07/30

第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 encodeURIdecodeURI 传入无效参数。 decodeURI('%')
EvalError eval 相关错误(现代 JS 已极少抛出)。 已几乎不再使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
eval('foo bar');
} catch (e) {
console.log(e instanceof SyntaxError); // true(在 eval 内部,可被捕获)
console.log(e.message); // 'Unexpected identifier'
}

try {
const obj = null;
obj.property;
} catch (e) {
console.log(e instanceof TypeError); // true
console.log(e.message); // "Cannot read properties of null (reading 'property')"
}

注意:代码解析阶段(在脚本加载时)的 SyntaxError 发生在编译阶段,此时 try-catch 尚未执行,因此无法捕获。只有运行时的语法错误(如 eval 内部的字符串解析)才能被捕获。

1.2 Error 实例的核心属性

属性 说明
message 错误描述信息。
name 错误类型的名称(如 'TypeError''Error')。
stack 错误发生时的调用栈字符串(非标准属性,但所有现代浏览器和 Node.js 都支持)。
cause ES2022 引入:可在 new Error(msg, { cause: originalError }) 中指定原始错误,形成错误链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function readFile(path) {
throw new Error(`Failed to read file: ${path}`);
}

try {
readFile('/data/config.json');
} catch (e) {
console.log(e.name); // 'Error'
console.log(e.message); // 'Failed to read file: /data/config.json'
console.log(e.stack);
// Error: Failed to read file: /data/config.json
// at readFile (file.js:2:9)
// at <anonymous> (file.js:6:3)
}

2. try-catch-finally 完整语法

2.1 基本结构

1
2
3
4
5
6
7
8
try {
// 可能抛出错误的代码
} catch (error) {
// 仅当 try 块中抛出错误时执行
// error 是捕获到的错误对象(可省略括号及参数)
} finally {
// 无论是否抛出错误,始终执行
}

catch 的参数可省略(ES2019):如果不需要访问错误对象,可以写 catch {}

2.2 finally 的执行时机

finally 块在 trycatch 执行完毕后始终运行,即使 trycatch 中有 return 语句。finallyreturn 语句生效之前执行,且 finally 中的 return 会覆盖 try/catch 的返回值。

1
2
3
4
5
6
7
8
9
10
function test() {
try {
return 1;
} catch {
return 2;
} finally {
return 3; // 覆盖 try 中的 return 1
}
}
console.log(test()); // 3 —— finally 的 return 覆盖了一切

最佳实践finally 中**避免使用 return**,它会使函数行为难以理解。finally 应仅用于清理资源(如关闭文件、清除定时器、恢复状态)。

2.3 catch 的条件捕获(非标准,但常见于某些引擎)

JavaScript 原生不支持带条件的 catch(如 catch (e if e instanceof TypeError))。需要条件判断时,在 catch 内部做类型检查并重新抛出:

1
2
3
4
5
6
7
8
9
10
try {
riskyOperation();
} catch (e) {
if (e instanceof TypeError) {
// 处理 TypeError
console.log('类型错误,已处理');
} else {
throw e; // 重新抛出无法处理的错误
}
}

2.4 try-catch 的性能代价

try 块中的代码在运行时会被优化器限制(因为需要保持异常路径的正确性)。不应将 try-catch 用于控制流,它仅应用于真正的异常情况。将整个应用逻辑包裹在 try-catch 中不是好的实践——应只包裹可能抛出错误的特定操作。


3. throw 与自定义错误

3.1 throw 可以抛出任意值

在 JavaScript 中,throw 可以抛出任何类型的值(字符串、数字、对象),但最佳实践是始终抛出 Error 或其子类的实例。因为只有 Error 实例才会自动包含调用栈信息(stack 属性)。

1
2
3
4
5
// ❌ 抛字符串:没有 stack,无法追踪来源
throw 'Invalid input';

// ✅ 抛 Error 实例:包含完整堆栈信息
throw new Error('Invalid input');

3.2 自定义错误类

扩展 Error 创建业务领域特定的错误类型,使错误处理更精确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError'; // 设置正确的错误名称
this.field = field;
// 确保正确的原型链(在 ES6 class 中通常自动处理,但某些环境需要手动设置)
}
}

class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
}
}

function validateUser(user) {
if (!user.name) {
throw new ValidationError('姓名不能为空', 'name');
}
if (user.age < 0 || user.age > 150) {
throw new ValidationError('年龄超出有效范围', 'age');
}
}

try {
validateUser({ name: '', age: 200 });
} catch (e) {
if (e instanceof ValidationError) {
console.error(`${e.field} 字段验证失败: ${e.message}`);
// name 字段验证失败: 姓名不能为空
} else {
throw e; // 不是验证错误,继续向上抛出
}
}

3.3 错误包装与错误链

在处理底层错误时,通常用更高级别的错误包装原始错误,并保留原始错误上下文(使用 cause 属性)。

1
2
3
4
5
6
7
8
9
10
11
12
13
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new ApiError(`请求失败: ${response.status}`, response.status);
}
return await response.json();
} catch (e) {
if (e instanceof ApiError) throw e;
// 将网络或解析错误包装为应用级错误
throw new Error(`无法获取用户数据 (ID: ${userId})`, { cause: e });
}
}

ES2022 的 cause 属性在 Node.js 16.9+ 和现代浏览器中可用。在错误日志中可递归打印 cause 链来追踪根本原因。


4. debugger 语句与开发者工具调试

4.1 debugger 语句

在代码中插入 debugger; 语句,当浏览器开发者工具打开时,代码执行会在该语句处暂停,进入调试模式。如果开发者工具未打开,该语句不会产生任何效果。

1
2
3
4
5
6
function calculate(a, b) {
const sum = a + b;
debugger; // 此处暂停,可检查 a、b、sum 的值
return sum * 2;
}
calculate(3, 5);

注意永远不要将 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
2
3
4
5
6
7
{
"version": 3,
"sources": ["src/utils.ts", "src/app.ts"],
"sourcesContent": ["原始 utils.ts 的内容...", "原始 app.ts 的内容..."],
"names": ["myVar", "calculate"],
"mappings": "AAAA,SAASA,GAAG;IACV..."
}

关键字段:

  • sources:原始源文件路径数组。
  • sourcesContent(可选):原始源文件的完整内容(嵌入后无需额外请求源文件)。
  • mappingsVLQ 编码的映射字符串,将压缩代码的每个 Token 映射到原始代码的行/列位置。

5.3 如何生成 Source Map

主流构建工具都内置了 Source Map 生成能力:

Vite / Webpack:配置 devtool 选项。

1
2
3
4
5
6
7
8
9
10
11
// vite.config.js
export default {
build: {
sourcemap: true, // 生产环境生成独立的 .map 文件
}
}

// webpack.config.js
module.exports = {
devtool: 'source-map', // 生产环境推荐,生成独立的 .map 文件
}

常用 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.onerrorunhandledrejection

1
2
3
4
5
6
7
8
9
10
11
// 捕获全局未处理的同步错误(以及部分资源加载错误)
window.addEventListener('error', (event) => {
console.error('全局错误:', event.message, '来源:', event.filename, '行:', event.lineno);
// 上报到错误追踪服务
});

// 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止控制台默认错误输出
});

6.2 Node.js 环境

1
2
3
4
5
6
7
8
9
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
// 记录日志后优雅退出——不建议在此之后恢复运行
process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});

注意:全局错误捕获是最后一道防线,不应依赖它来处理业务逻辑。它主要用于记录日志和优雅降级。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 以下哪种错误类型会在访问 null.property 时抛出?
    A. SyntaxError
    B. ReferenceError
    C. TypeError
    D. RangeError

  2. (单选) 关于 finally 块的说法,正确的是?
    A. finally 仅在 try 成功时执行。
    B. finally 中的 return 不会影响 try 的返回值。
    C. finallytrycatch 执行完毕后始终运行,即使有 return
    D. finally 块是可选的,没有意义。

  3. (填空) 要创建自定义错误类,应使用 ______ 关键字继承 Error,并在构造函数中设置 this.name 以标识错误类型。

  4. (多选) 以下哪些工具或方法可用于调试 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
2
3
4
5
6
7
try {
setTimeout(() => {
throw new Error('异步错误'); // 此错误不会被捕获
}, 0);
} catch (e) {
console.log('这里不会执行');
}

3. 正确的异步错误处理方式

  • Promise 内部:使用 .catch() 方法或 async/await 配合 try-catch(将 await 放在 try 块内)。
  • 回调模式:回调函数内部自行 try-catch 处理。
  • 全局兜底:监听 unhandledrejection 事件(浏览器)或 process.on('unhandledRejection')(Node.js)捕获未被任何 .catch 处理的 Promise 拒绝。
1
2
3
4
5
6
7
8
9
// async/await 正确捕获
async function fetchData() {
try {
const data = await fetch('/api').then(r => r.json());
// 使用 data
} catch (e) {
console.error('请求或解析失败:', e);
}
}

课后练习答案

一、概念自测答案

  1. C

    • 解析:在 null 上访问属性时 JavaScript 抛出 TypeError,因为操作试图在原始值上访问属性。
  2. C

    • 解析:finally 始终执行,即使 trycatch 中有 return。但 finally 中的 return 会覆盖 try/catch 的返回值。
  3. class ... extends

    • 解析:通过 class MyError extends Error { constructor(...) { super(...); this.name = 'MyError'; } } 创建自定义错误。
  4. 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)
  • 直接输出完整代码。”
CATALOG
  1. 1. 第54课:错误处理与调试——try-catch-finally、自定义错误、debugger、Source Map
    1. 1.1. 1. JavaScript 中的错误类型
      1. 1.1.1. 1.1 内置错误构造函数
      2. 1.1.2. 1.2 Error 实例的核心属性
    2. 1.2. 2. try-catch-finally 完整语法
      1. 1.2.1. 2.1 基本结构
      2. 1.2.2. 2.2 finally 的执行时机
      3. 1.2.3. 2.3 catch 的条件捕获(非标准,但常见于某些引擎)
      4. 1.2.4. 2.4 try-catch 的性能代价
    3. 1.3. 3. throw 与自定义错误
      1. 1.3.1. 3.1 throw 可以抛出任意值
      2. 1.3.2. 3.2 自定义错误类
      3. 1.3.3. 3.3 错误包装与错误链
    4. 1.4. 4. debugger 语句与开发者工具调试
      1. 1.4.1. 4.1 debugger 语句
      2. 1.4.2. 4.2 浏览器开发者工具的调试能力
    5. 1.5. 5. Source Map:生产代码的调试桥梁
      1. 1.5.1. 5.1 什么是 Source Map?
      2. 1.5.2. 5.2 Source Map 的结构
      3. 1.5.3. 5.3 如何生成 Source Map
      4. 1.5.4. 5.4 Source Map 的安全考量
    6. 1.6. 6. 全局错误捕获
      1. 1.6.1. 6.1 浏览器环境:window.onerror 与 unhandledrejection
      2. 1.6.2. 6.2 Node.js 环境
    7. 1.7. 课后练习
      1. 1.7.1. 一、概念自测(选择题 / 填空题)
      2. 1.7.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.7.3. 三、面试真题与参考答案
    8. 1.8. 课后练习答案
      1. 1.8.1. 一、概念自测答案
      2. 1.8.2. 二、AI 编程任务参考答案(提示词示例)