下面的代码会被try…catch 捕获吗?猜猜结果是啥?
try {
setTimeout(() => {
throw new Error('test');
}, 0);
} catch (err) {
console.log(err, '被捕获了');
}
console.log('代码继续执行...');
运行上述代码时,会发现控制台会输出 代码继续执行...
,然后会看到一个 Uncaught Error: test
(未捕获的错误)信息,而 被捕获了
这条消息则不会出现。
为什么会出现这种情况呢?
这个问题答案,需要深入理解JavaScript的运行时模型,事件循环(Event Loop) 和 try...catch
块的同步执行特性。
try...catch
块设计用于捕获 同步代码 中抛出的错误。当JavaScript引擎执行 try
块内的代码时,如果发生错误,它会立即停止 try
块的执行,并将控制权转移到 catch
块,从而处理错误。
关键点: try...catch
只能捕获在它执行 当前执行栈 内抛出的错误。
JavaScript 是单线程的,这意味着一次只能执行一个任务。然而,它通过事件循环机制实现了非阻塞的异步操作。
事件循环的核心组件包括:
调用栈(Call Stack)
- 存放正在执行的函数。执行顺序是后进先出(LIFO),代码一旦调用就入栈,执行完毕后弹出栈顶函数。
Web APIs(浏览器提供的异步API)
- 如 setTimeout、fetch、DOM事件等,当调用这些API时,它们会被交给浏览器后台处理,完成后回调函数会进入队列。
队列系统
- 宏任务队列(Macrotask Queue) :存放 setTimeout、setInterval、I/O操作等回调函数,优先级较低。
- 微任务队列(Microtask Queue) :存放 Promise.then、MutationObserver 等回调函数,优先级较高,每次事件循环优先清空此队列。
简单了解它们的执行流程
执行同步代码(如 console.log 语句)。
遇到异步操作(如 setTimeout)时:
交给浏览器API处理,完成后回调进入对应队列。
检查微任务队列:若有任务则全部执行完毕(如 Promise 回调)。
执行一个宏任务(如 setTimeout 回调)。
重复上述循环,直至所有任务完成。
所以为什么 setTimeout
中的错误未被捕获?
try {
setTimeout(() => {
throw new Error('test');
}, 0);
} catch (err) {
console.log(err, '被捕获了');
}
console.log('代码继续执行...');
根据上面的事件循环机制
- 当代码执行到 (A)
setTimeout
时,它是一个异步函数。setTimeout
本身会立即执行并完成,将匿名回调函数 () => { throw new Error('test'); }
注册到Web API(计时器)。
try...catch
块到此为止,已经完成了它的同步执行。此时,try...catch
块已经“退出”了,它所监听的执行上下文已经不存在了。
setTimeout
的回调函数(包含 throw new Error('test')
)会在计时器(0毫秒)结束后,被放入任务队列。
- 主线程继续执行
console.log('代码继续执行...');
同步代码。
- 当主线程所有同步代码执行完毕,调用栈清空后,事件循环从任务队列中取出
setTimeout
的回调函数,并将其推入调用栈执行。
- 此时,回调函数内的
throw new Error('test');
(B) 抛出错误。但是,由于这个错误是在一个 全新的执行上下文 中抛出的,这个上下文与之前 try...catch
所在的上下文是分离的。因此,之前的 try...catch
无法捕获到这个发生在“未来”的错误。
简而言之,try...catch
捕获的是 同步 发生的异常。setTimeout
的回调函数是在另一个 异步任务 中执行的,当该异步任务执行时,原始的 try...catch
块已经执行完毕并脱离了作用域。
像这种场景该如何捕获 setTimeout
中的异常错误?
既然外部的 try...catch
无法捕获,那么我们应该如何处理 setTimeout
回调函数中的错误呢?
将 try...catch
放入回调函数内部
最直接有效的方法是将 try...catch
块移动到异步回调函数内部:
setTimeout(() => {
try {
throw new Error('test');
} catch (err) {
console.log(err, '被捕获了 (在 setTimeout 内部)'); // 这行会执行
}
}, 0);
console.log('代码继续执行...');
使用 Promise(更推荐的异步错误处理方式)
在现代JavaScript中,处理异步操作和错误更推荐使用 Promise
和 async/await
。
使用 Promise 的 .catch()
:
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('test')); // 使用 reject 抛出错误
}, 0);
})
.catch(err => {
console.log(err, '被 Promise.catch 捕获了'); // 这行会执行
});
console.log('代码继续执行...');
总结
try...catch
只能捕获当前同步执行栈中的错误。对于异步操作,其回调函数将在未来不同的执行上下文中运行,因此它们内部抛出的错误无法被外部的 try...catch
捕获。
处理异步错误的最佳实践是:
- 在异步回调函数内部使用
try...catch
来捕获该回调中发生的同步错误。
- 拥抱 Promise 和
async/await
。