前端异常处理-常见处理方式

45 阅读6分钟

前言

前端异常处理,终极目的在于,万一应用哪个地方抛出了异常,也能用兜底的数据显示兜底的UI。做好了,可以极大降低白屏和错误页面的出现概率,极大降低线上事故率,提升用户体验。

  1. 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
    }]
    
    1. 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);
    }
    
    1. async await异步请求
    2. 正则表达式处理
    3. buffer处理
  1. 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")

注意:

  1. Promise自己的异常只能被自己catch, 或在try/catch里以await的方式调用来捕获。否则就会作为ERR_UNHANDLED_REJECTION异常抛出到全局。
  2. 外层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();
  1. 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);
  // 异常上报等异常处理
});
  1. 异步代码

接口请求异常

  1. 可以通过接口请求底层统一捕获和处理异常:

    1.   Fetch:fetch拦截, 用async await + try catch
    2.   Axios 请求:axios请求/响应拦截器,自行处理自定义的异常上报
  2. 具体接口,也可使用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);
    // 异常
  }
}
  1. 静态资源加载异常

  1. 全局静态资源异常监控

window.addEventListener("error") 或者资源标签的onError属性

  1. 图片异常处理

使用图片的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>
);}
  1. 其它静态资源加载异常

方法一:资源标签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)
  1. 页面崩溃异常

window.addEventListener('load',()=>{
    sessionStorage.setTitem('page_exit','pending')
})
window.addEventListener('beforeunload',()=>{
    sessionStorage.setTitem('page_exit','true')
})

sessionStorage.getTitem('page_exit')!='true' // 页面崩溃
  1. iframe异常

分三种情况:

  1. iframe 页面和你的主站是同域名:直接给 iframe 添加 onerror 事件即可。
  2. iframe 页面和你的主站不是同个域名,但是自己可以控制,可以通过与 iframe 通信的方式将异常信息抛给主站接收。与 iframe 通信的方式有很多,常用的如:postMessage,hash 或者 name 字段跨域等
  3. iframe 页面和你的主站不是同个域名,且网站不受自己控制为第三方的话,没办法捕获,这是出于安全性的考虑,只能通过控制台看到详细的错误信息