JavaScript 的 Error 对象

理解 JavaScript 的 Error 对象:超越 .stack 的全面解析

在日常的 JavaScript 开发中,处理错误就像处理 bug 一样是家常便饭。我们最熟悉的可能就是 Error 对象的 .message 属性了——它用最直白的话告诉我们出了什么问题。不过说实话,一个完整的 Error 对象包含的信息可比这丰富多了。除了常用的 .message,还有 .stack.name.cause 等属性,如果你能掌握这些关键属性,写出来的代码会更健壮,调试起来也会轻松不少。

.message:错误信息的简明扼要

.message 属性是个字符串,它用最直白的话告诉你”到底出了什么问题”。这可能是你在 catch 块里最先看到的东西。

当错误发生时,message 就像是错误的”自我介绍”。比如一个 TypeErrormessage 可能会说 "Cannot read properties of undefined",一看就知道是在访问 undefined 的属性。

1
2
3
4
5
6
try {
JSON.parse('invalid json');
} catch (error) {
// .message 提供了错误的简短描述
console.log(error.message); // 输出: "Unexpected token 'i', "invalid json" is not valid JSON"
}

.name:错误类型的身份证

.name 属性是个字符串,标识了错误的具体类型。JavaScript 内置了很多错误类型,像 TypeError(类型错误)、ReferenceError(引用错误)、SyntaxError(语法错误)等等。

通过 .name,我们可以精确判断错误类型,而不用去猜测或者解析错误消息。这样就能针对不同类型的错误做不同的处理,让代码更可控。

1
2
3
4
5
6
7
8
9
10
11
12
try {
throw new TypeError('Invalid type');
} catch (error) {
// 根据错误类型执行不同的处理逻辑
if (error.name === 'TypeError') {
console.log('这是类型错误,检查一下数据类型吧!');
} else if (error.name === 'ReferenceError') {
console.log('引用了不存在的变量');
} else {
console.log('其他类型的错误');
}
}

.stack:错误的”犯罪现场”重现

.stack 属性提供了调用堆栈跟踪,记录了导致错误发生的完整调用链路。这绝对是调试时最有用的信息,能帮你快速定位问题根源。

理解调用堆栈的工作原理

JavaScript 的函数调用遵循”后进先出”(Last-In, First-Out)的规则,由**调用堆栈(Call Stack)**管理:

  • 当函数被调用时,会被**压入(push)**到堆栈顶部
  • 当函数执行完毕返回时,会从堆栈中弹出(pop)

Error 对象被创建时,它会”拍快照”——捕获并保存当前调用堆栈的状态。

.stack 的输出格式

.stack 返回的是个字符串,虽然格式没有统一标准,但主流浏览器和 Node.js 的格式都很相似:

  1. 第一行:错误信息(类型 + 描述)
  2. 后续行:调用帧(Call Frames),按时间倒序排列
    • 最新的调用在最上面
    • 每个调用帧包含:函数名、文件名、行号和列号

实际案例分析

假设你有个多文件的项目,调用链是:app.jsapi.jsdata-processor.js

app.js

1
2
3
4
5
6
7
const { fetchDataAndProcess } = require('./api.js');

try {
fetchDataAndProcess();
} catch (e) {
console.error(e.stack);
}

api.js

1
2
3
4
5
6
7
8
const { processData } = require('./data-processor.js');

function fetchDataAndProcess() {
const data = null; // 模拟获取数据失败
processData(data); // 传入 null,会引发错误
}

module.exports = { fetchDataAndProcess };

data-processor.js

1
2
3
4
5
function processData(data) {
return data.map(item => item.name); // data 是 null,这里会抛出 TypeError
}

module.exports = { processData };

运行后你会看到类似这样的堆栈跟踪:

1
2
3
4
5
6
7
TypeError: Cannot read properties of null (reading 'map')
at processData (data-processor.js:2:16)
at fetchDataAndProcess (api.js:6:3)
at Object.<anonymous> (app.js:4:3)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
...

从这个 stack 信息可以清楚地看到:

  • 错误发生在 data-processor.js 第 2 行的 processData 函数
  • 该函数被 api.jsfetchDataAndProcess 调用
  • fetchDataAndProcess 又被 app.js 调用

这样你就能准确定位到 api.js 中传入 null 的那一行代码了。

.cause:错误的”前世今生”

.cause 是一个相对新的属性,它让你可以把多个错误”串联”起来。当你捕获到底层错误,但想抛出一个更有业务意义的新错误时,.cause 就能派上用场了。

这在复杂应用中特别有用,因为它能保持错误的完整上下文,让你沿着错误的”因果链”追溯到最初的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function loadConfig(path) {
try {
// 底层文件读取操作
const content = await fs.readFile(path, 'utf8');
return JSON.parse(content);
} catch (originalError) {
// 抛出更有意义的业务错误,但保留原始错误信息
throw new Error(`配置文件加载失败: ${path}`, {
cause: originalError
});
}
}

try {
await loadConfig('/config/app.json');
} catch (err) {
console.log(err.message); // "配置文件加载失败: /config/app.json"

if (err.cause) {
console.log('根本原因:', err.cause.message); // 原始的文件系统错误
}
}