前言
前端异常处理,终极目的在于,万一应用哪个地方抛出了异常,也能用兜底的数据显示兜底的UI。做好了,可以极大降低白屏和错误页面的出现概率,极大降低线上事故率,提升用户体验。
-
try-catch可疑代码块
注意:
能被 try catch 捕捉到的异常,必须是在报错的时候,线程执行已经进入 try catch 代码块,且处在 try catch 里面,这个时候才能被捕捉到。 如果是在之前,或者之后,都无法捕捉异常。
try catch能捕获捉到运行时非异步错误,无法捕获语法错误和异步错误。
所以try catch能捕获捉到运行时非异步错误,无法捕获语法错误和异步错误。适用于命令式代码,不适用于声明式如React等(React捕获异常后续会说明)。
try catch 不能捕获的js异常:
- 语法错误
- 普通异步任务如setTimeout
- Promise任务
- async任务需要await才能捕获
合理使用,不要过度使用。有得异常需要抛出去给外部处理。
常见的需要注意用try-catch包裹,捕获异常的情况
例子:
try { try_statements } [catch (exception) { catch_statements }] [finally { finally_statements }]
- JSON处理必须使用try catch捕获异常
try { const res=fetch(*) JSON.parse(res); // res 为服务端返回的数据 } catch(e) { // 捕获到详细的错误,在这里处理日志上报或其他异常处理等逻辑,如是否提示用户,是否有异常下的兜底数据,比如使用缓存数据等 console.error("服务端数据格式返回异常,无法解析", res); } // 注意:下面的异常try catch无法捕获 try { setTimeout(() => { undefined.map(v => v); }, 1000) } catch(e) { console.log("捕获到异常:", e); }
- async await异步请求
- 正则表达式处理
- buffer处理
-
Promise异常处理
注意:Promise 中的异常不能被 try-catch 和 window.onerror 捕获!(易错点!)
原因是,Promise 在执行回调中都用 try catch 包裹起来了,其中所有的异常都被内部捕获到了,并未往上抛异常。
局部Promise捕获两种方式:
// 1. Promise().catch() let promise = new Promise((resolve,reject)=>{}).catch(e=>{ // handle error }) // 2. async/await + try/catch let promise = new Promise(); async function test() { try { await promise; } catch (e) { // handle error } }
全局Promise异常用window.addEventListener("unhandledrejection")
注意:
- Promise自己的异常只能被自己catch, 或在try/catch里以await的方式调用来捕获。否则就会作为ERR_UNHANDLED_REJECTION异常抛出到全局。
- 外层Promise不能不能捕获内层Promise的异常。
let p1 = new Promise(async (resolve, reject) => {
return reject(100); // 被捕获
});
async function fn() {
try {
let result = await p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
let p1 = new Promise(async (resolve, reject) => {
return reject(100); // 未被捕获,会抛出全局异常:ERR_UNHANDLED_REJECTION
});
function fn() {
try {
let result = p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
let p1 = new Promise(async (resolve, reject) => {
console.log("after reject");
return Promise.reject(100); // 未被捕获,会抛出全局异常:ERR_UNHANDLED_REJECTION
});
async function fn() {
try {
let result = await p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
let p1 = new Promise(async (resolve, reject) => {
return Promise.reject(100); // 未被捕获,会抛出全局异常:ERR_UNHANDLED_REJECTION
}).catch((e) => {
console.log("promise out e", e); // 这里不会执行
});
async function fn() {
try {
let result = await p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
- Promise里同步抛出的异常,会触发Promise.reject而被捕获。但异步抛出的异常,不会触发Promise.reject,因此不会被捕获。
new Promise((resolve, reject) => {
throw new Error("Error1"); // 等效于reject
}).catch((e) => {
console.log("异常被捕获到了1");
});
new Promise(async (resolve, reject) => {
reject(new Error("Error2"));
}).catch((e) => {
console.log("异常被捕获到了2");
});
new Promise(async () => {
throw new Error("Error3");
}).catch((e) => {
console.log("异常被捕获到了3");
});
注意未被捕获的promise异常会作为全局异常抛出。
为了防止有漏掉的 Promise
异常,建议在全局增加一个对 unhandledrejection
的兜底监听,用来全局监听Uncaught Promise Error
。
window.addEventListener("unhandledrejection", function(e){
console.log(e);
// 异常上报等异常处理
});
-
异步代码
接口请求异常
-
可以通过接口请求底层统一捕获和处理异常:
- Fetch:fetch拦截, 用async await + try catch
- Axios 请求:axios请求/响应拦截器,自行处理自定义的异常上报
-
具体接口,也可使用async/await + try catch结合的方式, 或者使用promise.catch针对特殊的异常进行处理。
其它异步代码
使用async/await + try/catch
这里也可以使用相关babel插件,来给所有的async函数统一添加try/catch。感兴趣的同学可以了解下。
async function test() {
try {
const res = await requestWithTimeout(fakeAPI(), 0);
} catch (e) {
console.log("~~~e", e.message);
// 异常
}
}
-
静态资源加载异常
-
全局静态资源异常监控
window.addEventListener("error") 或者资源标签的onError属性
-
图片异常处理
使用图片的onError属性处理
<img
src="invalid_link"
onerror="this.onerror=null;this.src='https://www.test-img.com/img/default-avatar-png.png';"
>
如下面React中处理的例子,vue中同理。
const defaultAvatarUrl='https://www.test-img.com/img/default-avatar-png.png'
const Avatar = ({className, src, alt, ...props}) => {
const handleOnError = (e) => {
e.target.src = defaultAvatarUrl;
}
return (
<div>
<img
{...props}
className={`defaultClass ${className}`}
src={src || defaultAvatarUrl}
alt={alt}
onError={handleOnError}
/>
</div>
);}
-
其它静态资源加载异常
方法一:资源标签onerror 属性来捕获
function errorHandler(error) {
console.log("捕获到静态资源加载异常", error);
}
<script src="http://**/js/test.js" onerror="errorHandler(this)"></script>
<link rel="stylesheet" href="http://***/test.css" onerror="errorHandler(this)"/>
这样可以拿到静态资源的错误,但缺点很明显,代码的侵入性太强了,每一个静态资源标签都要加上 onerror 方法。
是否也可以通过 window.onerror 去全局监听加载失败呢?答案是否定的。因为 onerror 的事件并不会向上冒泡,window.onerror 接收不到加载失败的错误。只能通过该资源标签的onerror 方法,才可以。
冒泡不行,但是捕获阶段可以,也即addEventListener("error"),下面的方法二。
方法二:addEventListener("error")
我们可以通过捕获的方式全局监控加载失败的错误,虽然这也监控到了脚本错误,但通过 !(event instanceof ErrorEvent) 判断便可以筛选出加载失败的错误
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
-
页面崩溃异常
window.addEventListener('load',()=>{
sessionStorage.setTitem('page_exit','pending')
})
window.addEventListener('beforeunload',()=>{
sessionStorage.setTitem('page_exit','true')
})
sessionStorage.getTitem('page_exit')!='true' // 页面崩溃
-
iframe异常
分三种情况:
- iframe 页面和你的主站是同域名:直接给 iframe 添加 onerror 事件即可。
- iframe 页面和你的主站不是同个域名,但是自己可以控制,可以通过与 iframe 通信的方式将异常信息抛给主站接收。与 iframe 通信的方式有很多,常用的如:postMessage,hash 或者 name 字段跨域等
- iframe 页面和你的主站不是同个域名,且网站不受自己控制为第三方的话,没办法捕获,这是出于安全性的考虑,只能通过控制台看到详细的错误信息