错误机制
# 09.错误机制
# 目录介绍
# 9.1 错误类型
从ES3开始,JavaScript也提供了类似的异常处理机制,从而让JavaScript代码变的更健壮,即使执行的过程中出现了异常,也可以让程序具有了一部分的异常恢复能力。
JavaScript 异常机制的底层原理:JavaScript 引擎在执行 try 块时,会在调用栈中标记一个异常处理入口点。当 throw 语句执行时,引擎进行栈展开(Stack Unwinding)——从当前执行位置沿调用栈向上查找最近的 try...catch 块。如果找到,则跳转到对应的 catch 块执行;如果整个调用栈都没有 try...catch,错误就变成未捕获异常,触发全局错误处理(window.onerror 或 process.on('uncaughtException'))。注意:try...catch 只能捕获同步代码中的错误,异步回调(如 setTimeout)中的错误需要在回调内部单独捕获,或使用 Promise 的 .catch() / async/await + try...catch。
当错误发生时,JavaScript 提供了错误信息的内置 error 对象。
error 对象提供两个有用的属性:name 和 message 。
# 9.1.1 错误类型说明
JavaScript 中的错误分为以下几种类型:
Error:通用错误类型,其他错误类型都继承自它。SyntaxError:语法错误,通常在解析代码时发生。ReferenceError:引用错误,通常发生在访问未定义的变量时。TypeError:类型错误,通常发生在对错误类型的值进行操作时。RangeError:范围错误,通常发生在数值超出有效范围时。URIError:URI 错误,通常发生在处理 URI 时使用无效参数。EvalError:eval函数执行错误(已弃用)。
# 9.1.2 Error实例
JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例。
var err = new Error("出错了");
err.message // "出错了" m
2
上面代码中,我们调用Error()构造函数,生成一个实例对象err。Error()构造函数接受一个参数,表示错误提示,可以从实例的message属性读到这个参数。抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。
JavaScript 语言标准只提到,Error实例对象必须有message属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对Error实例还提供name和stack属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有。
- message:错误提示信息
- name:错误名称(非标准属性)
- stack:错误的堆栈(非标准属性)
Error 对象的继承体系:
Error(基类)
├── SyntaxError —— 语法解析错误
├── ReferenceError —— 引用不存在的变量
├── TypeError —— 类型使用错误
├── RangeError —— 数值超出范围
├── URIError —— URI 编码/解码错误
├── EvalError —— eval 相关(已弃用)
└── AggregateError —— 多个错误的集合(ES2021)
2
3
4
5
6
7
8
各错误类型的触发场景:
// SyntaxError —— 代码解析阶段就会报错
// eval('var 1abc;'); // SyntaxError: Invalid or unexpected token
// ReferenceError —— 访问未声明的变量
// console.log(undeclaredVar); // ReferenceError
// TypeError —— 对错误类型进行操作
// null.toString(); // TypeError: Cannot read properties of null
// (123)(); // TypeError: 123 is not a function
// RangeError —— 超出范围
// new Array(-1); // RangeError: Invalid array length
// function f() { f(); } f(); // RangeError: Maximum call stack size exceeded
// URIError —— URI 处理错误
// decodeURIComponent('%'); // URIError
// AggregateError(ES2021)
Promise.any([
Promise.reject(new Error('err1')),
Promise.reject(new Error('err2'))
]).catch(e => {
console.log(e instanceof AggregateError); // true
console.log(e.errors); // [Error: err1, Error: err2]
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
使用name和message这两个属性,可以对发生什么错误有一个大概的了解。stack属性用来查看错误发生时的堆栈。
function throwit() {
throw new Error('');
}
function catchit() {
try {
throwit();
} catch(e) {
console.log(e.stack); // print stack trace
}
}
catchit()
// Error
// at throwit (~/examples/throwcatch.js:9:11)
// at catchit (~/examples/throwcatch.js:3:9)
// at repl:1:5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面代码中,错误堆栈的最内层是throwit函数,然后是catchit函数,最后是函数的运行环境。
# 9.1.3 自定义错误
可以通过继承 Error 类创建自定义错误类型:
class CustomError extends Error {
constructor(message) {
super(message);
this.name = "CustomError";
}
}
try {
throw new CustomError("This is a custom error.");
} catch (error) {
if (error instanceof CustomError) {
console.error("CustomError:", error.message);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
实际项目中的自定义错误体系:
// 基础业务错误
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.name = 'AppError';
this.code = code; // 业务错误码
this.statusCode = statusCode; // HTTP 状态码
this.timestamp = new Date().toISOString();
}
}
// 具体业务错误
class ValidationError extends AppError {
constructor(field, message) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
this.field = field;
}
}
class AuthenticationError extends AppError {
constructor(message = '未授权访问') {
super(message, 'AUTH_ERROR', 401);
this.name = 'AuthenticationError';
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} 不存在`, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
this.resource = resource;
}
}
// 使用
function getUser(id) {
if (!id) throw new ValidationError('id', 'ID 不能为空');
const user = db.find(id);
if (!user) throw new NotFoundError('User');
return user;
}
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
37
38
39
40
41
42
# 9.2 throw抛出错误
# 9.2.1 throw用法
使用 throw 关键字抛出错误。
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed!");
}
return a / b;
}
console.log(divide(10, 0)); // 抛出错误
2
3
4
5
6
7
8
实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值。
// 抛出一个字符串
throw 'Error!';
// Uncaught Error!
// 抛出一个数值
throw 42;
// Uncaught 42
// 抛出一个布尔值
throw true;
// Uncaught true
// 抛出一个对象
throw {
toString: function () {
return 'Error!';
}
};
// Uncaught {toString: ƒ}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值。
# 9.2.2 throw建议
- 错误类型:尽量抛出
Error对象或其子类,而不是字符串或其他类型。 - 错误信息:错误信息应清晰明确,便于调试。
- 异常处理:使用
try...catch捕获和处理错误,避免程序崩溃。
# 9.3 捕获错误
try...catch 是 JavaScript 中最常用的错误处理机制,用于捕获同步代码中的异常。
语法
try {
// 可能抛出错误的代码
} catch (error) {
// 捕获并处理错误
} finally {
// 无论是否发生错误,都会执行的代码
}
2
3
4
5
6
7
# 9.3.1 捕获案例
示例
try {
let result = riskyOperation();
console.log("Result:", result);
} catch (error) {
console.error("An error occurred:", error.message);
} finally {
console.log("Execution completed.");
}
2
3
4
5
6
7
8
特点
try块中的代码会被执行,如果抛出错误,则进入catch块。catch块中的error参数是一个错误对象,通常包含message和name属性。finally块中的代码无论是否发生错误都会执行,通常用于清理资源。
# 9.3.2 异步捕获
在异步代码(如 Promise 或 async/await)中,异常捕获的方式略有不同。
async function fetchData() {
try {
let response = await fetch("https://example.com/data");
let data = await response.json();
console.log(data);
} catch (error) {
console.error("捕获到错误:", error.message);
}
}
fetchData();
2
3
4
5
6
7
8
9
10
11
# 9.3.3 捕获原理
如果 try 块中的代码正常执行,catch 块会被跳过。
如果 try 块中的代码抛出错误,程序会立即跳转到 catch 块,并执行其中的代码。
无论是否发生错误,finally 块中的代码都会执行。
# 9.3.4 try...catch 的性能影响
疑惑:使用 try...catch 会影响性能吗?
答疑:在现代 V8 引擎中,try...catch 本身的性能开销已经非常小。但需要注意几点:
论证:
// 以前(旧版 V8):try...catch 内的代码无法被 JIT 优化
// 现在(V8 TurboFan):try...catch 内的代码可以正常优化
// 但是,不建议将整个函数体包裹在 try...catch 中
// 不推荐
function processAll(data) {
try {
// 200 行代码...
} catch (e) {
handleError(e);
}
}
// 推荐:只包裹可能出错的代码
function processAll(data) {
const validated = validate(data); // 不需要 try...catch
try {
const result = riskyOperation(validated);
} catch (e) {
handleError(e);
}
cleanup(); // 确定不会出错的代码
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
结果展示:合理使用 try...catch,将其范围缩小到可能出错的代码段。过大的 try 块会降低代码可读性,也不利于精确定位错误。
# 9.4 finally
try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。无论是否发生错误都会执行,通常用于清理资源。
function cleansUp() {
try {
throw new Error('出错了……');
console.log('此行不会执行');
} finally {
console.log('完成清理工作');
}
}
cleansUp()
// 完成清理工作
// Uncaught Error: 出错了……
// at cleansUp (<anonymous>:3:11)
// at <anonymous>:10:1
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码中,由于没有catch语句块,一旦发生错误,代码就会中断执行。中断执行之前,会先执行finally代码块,然后再向用户提示报错信息。
function idle(x) {
try {
console.log(x);
return 'result';
} finally {
console.log('FINALLY');
}
}
idle('hello')
// hello
// FINALLY
2
3
4
5
6
7
8
9
10
11
12
finally 的特殊行为:
// finally 中的 return 会覆盖 try/catch 中的 return
function foo() {
try {
return 1;
} catch (e) {
return 2;
} finally {
return 3; // 这个 return 会覆盖上面的所有 return
}
}
console.log(foo()); // 3
// finally 中的 return 甚至会"吞掉"错误
function bar() {
try {
throw new Error('错误');
} finally {
return '正常'; // 错误被吞掉,不会抛出
}
}
console.log(bar()); // "正常"(错误消失了!)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
最佳实践:永远不要在 finally 中使用 return,它会导致难以追踪的 bug。
# 9.5 异步错误处理
# 9.5.1 Promise错误处理
使用 .catch() 捕获 Promise 中的错误。
fetch("https://example.com/data")
.then((response) => response.json())
.catch((error) => {
console.error("Fetch error:", error);
});
2
3
4
5
# 9.5.2 async/await错误处理
使用 try...catch 捕获 async/await 中的错误。
async function fetchData() {
try {
const response = await fetch("https://example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Fetch error:", error);
}
}
fetchData();
2
3
4
5
6
7
8
9
10
11
# 9.5.3 错误处理模式:Result类型
疑惑:每次调用异步函数都要写 try...catch,代码很冗长。有没有更优雅的方式?
答疑:可以借鉴 Go/Rust 语言的"返回错误而非抛出错误"模式,将 try...catch 封装为工具函数:
论证:
// 工具函数:将 async 函数的错误转为返回值
async function to(promise) {
try {
const result = await promise;
return [null, result];
} catch (error) {
return [error, null];
}
}
// 使用
async function fetchUserData(userId) {
const [err, response] = await to(fetch(`/api/users/${userId}`));
if (err) {
console.error('网络错误:', err);
return null;
}
const [parseErr, data] = await to(response.json());
if (parseErr) {
console.error('解析错误:', parseErr);
return null;
}
return data;
}
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
结果展示:这种模式消除了深层嵌套的 try...catch,使错误处理更加线性和明确。许多 Node.js 库(如 await-to-js)都提供了类似的工具。
# 9.6 全局错误处理
# 9.6.1 window.onerror
捕获全局未处理的错误。
window.onerror = function (message, source, lineno, colno, error) {
console.error("Global error:", message, "at", source, "line", lineno);
return true; // 阻止默认错误提示
};
2
3
4
# 9.6.2 unhandledrejection
捕获未处理的 Promise 拒绝。
window.addEventListener("unhandledrejection", function (event) {
console.error("Unhandled rejection:", event.reason);
});
2
3
# 9.6.3 调试错误
console.error():打印错误信息。console.trace():打印调用堆栈。- 开发者工具:使用浏览器的开发者工具调试错误。
# 9.6.4 前端错误监控方案
在生产环境中,需要一套完整的错误监控体系来收集和上报错误:
// 统一的错误监控类
class ErrorMonitor {
constructor(reportUrl) {
this.reportUrl = reportUrl;
this.init();
}
init() {
// 1. 捕获同步错误
window.onerror = (message, source, lineno, colno, error) => {
this.report({
type: 'javascript',
message, source, lineno, colno,
stack: error?.stack
});
return false; // 不阻止默认处理
};
// 2. 捕获 Promise 未处理的拒绝
window.addEventListener('unhandledrejection', (event) => {
this.report({
type: 'promise',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack
});
});
// 3. 捕获资源加载错误(window.onerror 无法捕获)
window.addEventListener('error', (event) => {
if (event.target !== window) {
this.report({
type: 'resource',
tagName: event.target.tagName,
src: event.target.src || event.target.href
});
}
}, true); // 必须在捕获阶段
}
report(errorInfo) {
const data = {
...errorInfo,
url: location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
};
// 使用 navigator.sendBeacon 保证页面卸载时也能发送
navigator.sendBeacon(this.reportUrl, JSON.stringify(data));
}
}
// 初始化
new ErrorMonitor('/api/errors/report');
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 9.7 异常处理流程
在 JavaScript 中,未捕获的错误(Uncaught Error) 是指没有被 try...catch 语句捕获的异常。
这类错误会触发 JavaScript 引擎的默认错误处理机制,通常会导致程序终止并打印错误信息。
# 9.7.1 未捕获错误处理流程
(1)错误抛出:当代码中发生错误(如 throw new Error("Something went wrong"))且没有被 try...catch 捕获时,错误会被抛出。
(2)错误传播:错误会沿着调用栈向上传播,直到找到最近的 try...catch 块。如果调用栈中没有 try...catch 块,错误会继续传播到全局作用域。
(3)全局错误处理:如果错误传播到全局作用域,JavaScript 引擎会触发全局错误处理机制:
- 在浏览器中,会触发
window.onerror事件。 - 在 Node.js 中,会触发
process.on('uncaughtException')事件。
(4)程序终止:如果全局错误处理机制也没有捕获错误,JavaScript 引擎会终止程序的执行,并打印错误信息到控制台。
# 9.7.2 核心原理
(1)调用栈(Call Stack):JavaScript 使用调用栈来管理函数的执行顺序。当发生错误时,引擎会沿着调用栈向上查找是否有 try...catch 块来处理错误。
(2)事件循环(Event Loop):在异步代码中,错误可能发生在事件循环的不同阶段。如果错误未被捕获,它会被传递到全局作用域。
(3)全局错误事件:JavaScript 提供了全局错误事件来捕获未处理的错误:
- 浏览器:
window.onerror和window.addEventListener('error', ...)。 - Node.js:
process.on('uncaughtException')和process.on('unhandledRejection')。
# 9.7.3 代码示例
(1)同步代码中的未捕获错误
function foo() {
throw new Error("Oops!");
}
function bar() {
foo();
}
bar(); // 未捕获错误,程序终止
2
3
4
5
6
7
8
9
(2)异步代码中的未捕获错误
setTimeout(() => {
throw new Error("Oops!");
}, 1000); // 未捕获错误,程序终止
2
3
(3)全局错误处理(浏览器)
window.onerror = function (message, source, lineno, colno, error) {
console.log("捕获到全局错误:", message);
return true; // 阻止默认错误处理
};
throw new Error("Oops!"); // 错误被全局处理,程序不会终止
2
3
4
5
6
(4)全局错误处理(Node.js)
process.on('uncaughtException', (err) => {
console.log("捕获到未处理的异常:", err.message);
});
throw new Error("Oops!"); // 错误被全局处理,程序不会终止
2
3
4
5
# 9.7.4 处理建议
(1)使用 try...catch:在可能抛出错误的代码块中使用 try...catch 捕获错误。
try {
throw new Error("Oops!");
} catch (error) {
console.log("捕获到错误:", error.message);
}
2
3
4
5
(2)全局错误处理:在全局作用域中注册错误处理函数,捕获未处理的错误。
// 浏览器
window.addEventListener('error', (event) => {
console.log("捕获到全局错误:", event.message);
});
// Node.js
process.on('uncaughtException', (err) => {
console.log("捕获到未处理的异常:", err.message);
});
2
3
4
5
6
7
8
9
3)Promise 错误处理:使用 .catch() 或 try...catch(在 async/await 中)捕获 Promise 中的错误。
// 使用 .catch()
Promise.reject(new Error("Oops!")).catch((error) => {
console.log("捕获到 Promise 错误:", error.message);
});
// 使用 async/await
(async () => {
try {
await Promise.reject(new Error("Oops!"));
} catch (error) {
console.log("捕获到错误:", error.message);
}
})();
2
3
4
5
6
7
8
9
10
11
12
13
4)避免静默失败:确保所有错误都被捕获和处理,避免程序静默失败。
# 9.7.5 避免静默失败
| 阶段 | 描述 |
|---|---|
| 错误抛出 | 代码中发生错误且未被捕获。 |
| 错误传播 | 错误沿着调用栈向上传播,寻找 try...catch 块。 |
| 全局错误处理 | 如果错误未被捕获,触发全局错误事件(如 window.onerror)。 |
| 程序终止 | 如果全局错误处理也未捕获错误,程序终止并打印错误信息。 |