3、JavaScript异常捕获:try-catch-finally、window.onerror、Promise异常处理、async/await异常处理
JavaScript的异常处理,说白了就是给代码上「保险」。
我做了这么多年混合开发,见过太多因为一个未捕获的异常导致整个页面白屏的惨案。你想想看,用户正用着你的App,突然页面卡死,那体验得多糟糕。
所以今天咱们就把JS异常捕获这件事彻底聊透。
3.1 try-catch-finally:最基础的防线
这是最经典的异常捕获方式。我个人习惯在可能出错的同步代码块外面包一层try-catch,就像给代码穿防弹衣。
try {
// 可能会出错的代码
let result = riskyOperation();
console.log('操作成功:', result);
} catch (error) {
// 出错后的处理
console.error('捕获到异常:', error.message);
// 上报异常
reportError(error);
} finally {
// 无论是否出错都会执行
console.log('清理工作完成');
}
这里有个细节要注意:finally里的代码一定会执行,哪怕try或catch里有return语句。我在项目中遇到过这样一个坑——
我曾经踩过的坑:在finally里又抛了一个异常,结果把catch里捕获的异常给覆盖了。所以finally里尽量只做清理工作,别搞复杂逻辑。
| 场景 | try执行 | catch执行 | finally执行 |
|---|---|---|---|
| 无异常 | ✅ | ❌ | ✅ |
| 有异常 | 中断 | ✅ | ✅ |
| try中有return | ✅ | ❌ | ✅(return前执行) |
3.2 window.onerror:全局的兜底网
try-catch只能捕获它包裹的代码块里的异常。那漏网之鱼怎么办?
这时候就需要window.onerror出马了。它就像一个全局的异常监听器,任何未被try-catch捕获的JS异常,都会触发它。
window.onerror = function(message, source, lineno, colno, error) {
console.error('全局异常捕获:', {
message: message,
source: source,
line: lineno,
column: colno,
stack: error ? error.stack : '无堆栈信息'
});
// 上报到日志系统
reportGlobalError({
message,
source,
lineno,
colno,
stack: error?.stack
});
// 返回true表示异常已被处理,浏览器不再打印默认错误
return true;
};
重要提示:window.onerror有个限制——它捕获不到跨域脚本的详细错误信息。如果你引用了CDN上的脚本,记得加上crossorigin="anonymous"属性,否则你只能看到"Script error."这种毫无价值的信息。
嗯,这里要注意:window.onerror也捕获不到Promise里的异常。那Promise的异常怎么处理?往下看。
3.3 Promise异常处理:.catch()与unhandledrejection
Promise的出现让异步编程变得优雅,但也带来了新的异常处理问题。
最基本的做法是用.catch()捕获:
fetch('/api/data')
.then(response => response.json())
.then(data => {
// 处理数据
processData(data);
})
.catch(error => {
// 捕获fetch或处理过程中的任何异常
console.error('请求失败:', error);
});
但问题来了——如果某个Promise忘了加.catch(),异常就会静默消失。你想想看,这多可怕?
所以浏览器提供了unhandledrejection事件,专门捕获那些没有被处理的Promise异常:
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的Promise异常:', event.reason);
// 上报异常
reportUnhandledRejection(event.reason);
// 阻止默认处理(浏览器控制台打印)
event.preventDefault();
});
我的建议:在项目初始化时就注册unhandledrejection监听器,把它和window.onerror一起作为全局异常捕获的「双保险」。
3.4 async/await异常处理:try-catch的优雅回归
async/await让异步代码写起来像同步一样。异常处理也回归到了最熟悉的try-catch模式。
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取用户数据失败:', error);
// 可以在这里做降级处理
return getCachedUserData(userId);
}
}
这里有个很多人容易忽略的点——await后面的Promise如果reject了,会被try-catch捕获到。但如果你忘了写try-catch,那这个异常就会变成unhandledrejection。
我曾经踩过的坑:在async函数里用了多个await,但只在外层包了一个try-catch。结果第一个await就抛异常了,后面的代码全没执行。后来我改成每个关键操作单独try-catch,或者用Promise.allSettled来处理。
3.5 知识体系总览
说了这么多,咱们用一张图来总结一下JS异常捕获的完整体系:
3.6 实战建议
说了这么多理论,最后给几条实战建议:
- 分层捕获:不要只依赖全局捕获。每个关键操作都加上try-catch,精确控制异常处理逻辑。
- 统一上报:所有捕获到的异常,最终都要上报到你的日志系统。我习惯封装一个reportError函数,统一处理。
- 不要吞异常:catch里只console.error是不够的,一定要做点什么——要么上报,要么降级,要么重试。
- 区分异常类型:网络错误、业务错误、代码错误,处理方式完全不同。建议用自定义错误类来区分。
我的个人习惯:在项目入口文件里同时注册window.onerror和unhandledrejection,然后在每个模块的关键函数里用try-catch包裹。这样既有全局兜底,又有局部精确控制,基本不会漏掉任何异常。
好了,JS异常捕获这块就聊到这儿。记住一句话:异常捕获不是为了让代码不出错,而是为了出错时我们能知道、能处理、能恢复。