深入理解 JavaScript 异步错误处理机制,掌握 try-catch、Promise.catch、全局错误监听等多种捕获方式,构建健壮的异步代码。
一、背景与问题
异步编程中的错误处理是前端开发的重要难点:
1
2
3
4
5
6
7
8
| // ❌ 常见错误:try-catch 无法捕获异步错误
try {
setTimeout(() => {
throw new Error('异步错误'); // 无法被捕获!
}, 100);
} catch (error) {
console.error(error); // 永远不会执行
}
|
核心问题:
- try-catch 只能捕获同步错误
- Promise 错误会静默失败(不处理时)
- async/await 错误处理方式多样
- 全局错误监听机制复杂
二、核心概念与定义
2.1 同步错误 vs 异步错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 同步错误:立即抛出,可以被 try-catch 捕获
try {
throw new Error('同步错误');
} catch (error) {
console.log('捕获成功:', error.message);
}
// 异步错误:在未来的调用栈中抛出,无法被当前 try-catch 捕获
try {
setTimeout(() => {
throw new Error('异步错误'); // 调用栈已不同
}, 0);
} catch (error) {
console.log('不会执行');
}
|
2.2 错误冒泡机制
Promise 中的错误会沿着 Promise 链向下冒泡,直到遇到 .catch():
1
2
3
4
5
6
7
| Promise.resolve()
.then(() => { throw new Error('错误1'); }) // 抛出
.then(() => { /* 不会执行 */ }) // 跳过
.then(() => { /* 不会执行 */ }) // 跳过
.catch(error => { // 捕获
console.log(error.message); // '错误1'
});
|
2.3 错误类型
| 错误类型 | 触发场景 | 捕获方式 |
|---|
Error | 通用错误 | try-catch / .catch() |
TypeError | 类型错误 | try-catch / .catch() |
ReferenceError | 引用错误 | try-catch / .catch() |
SyntaxError | 语法错误 | 无法捕获(解析阶段) |
AggregateError | 多个错误集合 | Promise.any() 失败 |
三、最小示例
3.1 Promise 错误处理
1
2
3
4
5
6
7
8
9
10
11
12
| // 方式一:.catch() 方法
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('请求失败:', error));
// 方式二:then 的第二个参数
fetch('/api/data')
.then(
response => response.json(),
error => console.error('请求失败:', error) // 捕获 fetch 错误
);
|
3.2 async/await 错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 方式一:try-catch
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error; // 可以选择重新抛出
}
}
// 方式二:.catch() 链式调用
async function fetchData() {
const data = await fetch('/api/data')
.then(r => r.json())
.catch(error => {
console.error('获取数据失败:', error);
return null; // 返回默认值
});
return data;
}
|
四、核心知识点拆解
4.1 try-catch 的作用域限制
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
| // ❌ 错误示范:无法捕获回调中的错误
try {
[1, 2, 3].forEach(num => {
if (num === 2) throw new Error('错误');
});
} catch (error) {
console.log('捕获:', error); // 可以捕获!forEach 是同步的
}
// ❌ 错误示范:无法捕获异步回调中的错误
try {
setTimeout(() => {
throw new Error('异步错误'); // 无法捕获
}, 0);
} catch (error) {
console.log('不会执行');
}
// ✅ 正确方式:在回调内部捕获
setTimeout(() => {
try {
throw new Error('异步错误');
} catch (error) {
console.log('捕获:', error);
}
}, 0);
|
4.2 Promise 错误冒泡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 错误会沿着 Promise 链冒泡
const promise = new Promise((resolve, reject) => {
reject(new Error('初始错误'));
});
promise
.then(value => {
console.log('不会执行');
return value;
})
.then(value => {
console.log('不会执行');
return value;
})
.catch(error => {
console.log('捕获:', error.message); // '初始错误'
return '恢复值'; // 返回值会传递给后续 then
})
.then(value => {
console.log('恢复后:', value); // '恢复值'
});
|
4.3 async/await 错误处理模式
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
| // 模式一:包装函数
async function safeAsync(fn) {
try {
return [null, await fn()];
} catch (error) {
return [error, null];
}
}
// 使用(类似 Go 语言风格)
const [error, data] = await safeAsync(() => fetchData());
if (error) {
console.error(error);
} else {
console.log(data);
}
// 模式二:错误边界函数
function withErrorHandler(fn, handler) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
return handler(error, ...args);
}
};
}
const safeFetch = withErrorHandler(
fetchData,
(error) => {
console.error(error);
return { data: null, error };
}
);
|
4.4 全局错误监听
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
| // 1. window.onerror(传统方式)
window.onerror = (message, source, lineno, colno, error) => {
console.error('全局错误:', { message, source, lineno, colno, error });
return true; // 阻止默认行为
};
// 2. window.addEventListener('error')
window.addEventListener('error', (event) => {
console.error('错误事件:', event.error);
});
// 3. Promise 未捕获错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止控制台警告
});
// 4. Node.js 环境
process.on('uncaughtException', (error) => {
console.error('未捕获异常:', error);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});
|
五、实战案例
5.1 统一错误处理类
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
37
38
39
40
41
42
43
44
45
| class ErrorHandler {
constructor(options = {}) {
this.handlers = new Map();
this.fallback = options.fallback || console.error;
}
// 注册错误处理器
register(errorType, handler) {
this.handlers.set(errorType, handler);
return this;
}
// 处理错误
handle(error) {
// 查找匹配的处理器
for (const [ErrorType, handler] of this.handlers) {
if (error instanceof ErrorType) {
return handler(error);
}
}
// 使用默认处理器
return this.fallback(error);
}
// 包装异步函数
wrap(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
return this.handle(error);
}
};
}
}
// 使用示例
const errorHandler = new ErrorHandler()
.register(TypeError, (e) => console.warn('类型错误:', e.message))
.register(ReferenceError, (e) => console.warn('引用错误:', e.message));
const safeFn = errorHandler.wrap(async () => {
const data = await fetchData();
return data.name.toUpperCase(); // 可能抛出 TypeError
});
|
5.2 API 请求错误处理
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(path, options = {}) {
try {
const response = await fetch(`${this.baseURL}${path}`, options);
// 检查 HTTP 状态
if (!response.ok) {
throw new HttpError(response.status, response.statusText);
}
return await response.json();
} catch (error) {
// 网络错误
if (error instanceof TypeError && error.message.includes('fetch')) {
throw new NetworkError('网络请求失败');
}
// 重新抛出
throw error;
}
}
async get(path) {
return this.request(path, { method: 'GET' });
}
async post(path, data) {
return this.request(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
}
// 自定义错误类型
class HttpError extends Error {
constructor(status, statusText) {
super(`HTTP ${status}: ${statusText}`);
this.name = 'HttpError';
this.status = status;
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
|
5.3 React 错误边界
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 ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 上报错误
console.error('组件错误:', error, errorInfo);
// 可以发送到错误监控服务
// reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div>
<h1>出错了</h1>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
);
}
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<div>加载失败</div>}>
<MyComponent />
</ErrorBoundary>
|
六、底层原理
6.1 错误对象的创建与传播
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
| // Error 对象结构
const error = new Error('错误信息');
console.log(error);
// {
// name: 'Error',
// message: '错误信息',
// stack: 'Error: 错误信息\n at <anonymous>:1:15'
// }
// 错误传播:调用栈
function a() {
b();
}
function b() {
c();
}
function c() {
throw new Error('错误');
}
try {
a();
} catch (error) {
console.log(error.stack);
// Error: 错误
// at c (<anonymous>:9:11)
// at b (<anonymous>:6:5)
// at a (<anonymous>:3:5)
// at <anonymous>:13:5
}
|
6.2 Promise 错误处理机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Promise 内部错误处理
new Promise((resolve, reject) => {
// 同步抛出错误会被捕获并转为 reject
throw new Error('错误');
})
.catch(error => {
console.log('捕获:', error.message);
});
// 等价于
new Promise((resolve, reject) => {
reject(new Error('错误'));
})
.catch(error => {
console.log('捕获:', error.message);
});
|
6.3 微任务队列中的错误
1
2
3
4
5
6
7
8
| // Promise.then 中的错误
Promise.resolve()
.then(() => {
throw new Error('微任务错误');
});
// 错误在微任务队列中传播
// 如果没有 .catch(),会在微任务队列清空后触发 unhandledrejection
|
七、高频面试题解析
Q1:为什么 try-catch 无法捕获 setTimeout 中的错误?
答案:因为 setTimeout 的回调在新的调用栈中执行,try-catch 只能捕获同一调用栈中的错误。
1
2
3
4
| // 调用栈示意
// main() -> try -> setTimeout(注册) -> catch(已退出)
// ... 时间流逝 ...
// timer() -> callback() -> throw Error // 新调用栈,无 catch
|
Q2:Promise 的错误会静默失败吗?
答案:会。如果 Promise 被 reject 且没有 .catch() 处理,错误会静默失败(现代浏览器会触发 unhandledrejection 事件)。
1
2
3
4
5
6
| // ❌ 错误会静默失败
Promise.reject(new Error('错误'));
// ✅ 正确做法
Promise.reject(new Error('错误'))
.catch(error => console.error(error));
|
Q3:如何实现 Go 风格的错误处理?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Go 风格:返回 [error, value]
async function go(asyncFn) {
try {
const result = await asyncFn();
return [null, result];
} catch (error) {
return [error, null];
}
}
// 使用
const [err, data] = await go(() => fetchData());
if (err) {
console.error(err);
return;
}
console.log(data);
|
Q4:async 函数返回的 Promise 如何处理错误?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| async function fn() {
throw new Error('错误');
}
// 方式一:await + try-catch
try {
await fn();
} catch (error) {
console.error(error);
}
// 方式二:.catch()
await fn().catch(error => console.error(error));
// 方式三:不 await,让错误冒泡
return fn(); // 错误会传递给调用者
|
八、总结与扩展
核心要点
- try-catch 限制:只能捕获同步错误,无法捕获异步回调中的错误
- Promise 错误冒泡:错误沿 Promise 链向下传播,直到遇到
.catch() - async/await:可以用 try-catch 捕获 await 表达式的错误
- 全局监听:
unhandledrejection 捕获未处理的 Promise 错误
最佳实践
- 永远处理 Promise 错误:每个 Promise 链末尾加
.catch() - 统一错误类型:自定义 HttpError、NetworkError 等
- 错误上报:全局错误监听 + 上报服务
- 优雅降级:错误时显示友好提示,而非白屏
扩展阅读
- Error Cause:ES2022 错误链
new Error('message', { cause: originalError }) - AggregateError:
Promise.any() 失败时返回多个错误 - Sentry:前端错误监控平台
- Error Boundary:React 组件级错误处理
掌握异步错误处理机制,是编写健壮前端代码的基础能力。