前端错误捕获

132 阅读5分钟

背景

当项目上线后,如何快速精准的定位问题?前端一般会有一套监控系统,如sentry。它可以监控程序代码中出现的报错问题,生成issue,通过查看issue快速定位到问题发生的位置。此文,主要介绍一些关于错误捕获。

sentry原理:Javascript代码发生错误后,Javascript引擎会抛出一个Error对象,并触发window.onerror事件,Sentry正是对window.onerror进行重写,实现错误监控的逻辑,并添加了很多信息帮助错误定位,并对错误进行滚跨浏览器的兼容等

try/catch

只能捕获代码常规的运行错误,语法错误和异步错误不能捕获到

// 示例1:常规运行时错误,可以捕获 ✔
 try {
   let a = undefined;
   if (a.length) {
     console.log('111');
   }
 } catch (e) {
   console.log('捕获到异常:', e);
}

// 示例2:语法错误,不能捕获 ❌  
try {
  const notdefined,
} catch(e) {
  console.log('捕获不到异常:', 'Uncaught SyntaxError');
}
  
// 示例3:异步错误,不能捕获 ❌
try {
  setTimeout(() => {
    console.log(notdefined);
  }, 0)
} catch(e) {
  console.log('捕获不到异常:', 'Uncaught ReferenceError');
}

window.onerror

window.onerror 可以捕获常规错误、异步错误,但不能捕获资源错误

可以收集到错误字符串信息、发生错误的js文件,错误所在的行数、列数、和Error对象(里面会有调用堆栈信息等)

window.onerror = function(message, source, lineno, colno, error) {
  console.log("捕获到的错误信息是:", message, source, lineno, colno, error);
};

// 示例1:常规运行时错误,可以捕获 ✅
console.log(notdefined);

// 示例2:语法错误,不能捕获 
const notdefined;

// 示例3:异步错误,可以捕获 ✅
setTimeout(() => {
  console.log(notdefined);
}, 0);

// 示例4:资源错误,不能捕获 
let script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://www.test.com/index.js";
document.body.appendChild(script);

Promise错误

Promise中抛出的错误,无法被 window.onerror、try/catch、 error 事件捕获到,可通过 unhandledrejection 事件来处理

try {
  new Promise((resolve, reject) => {
    JSON.parse("");
    resolve();
  });
} catch (err) {
  // try/catch 不能捕获Promise中错误 
  console.error("in try catch", err);
}

// error事件 不能捕获Promise中错误 
window.addEventListener(
  "error",
  error => {
    console.log("捕获到异常:", error);
  },
  true
);

// window.onerror 不能捕获Promise中错误 
window.onerror = function(message, source, lineno, colno, error) {
  console.log("捕获到异常:", { message, source, lineno, colno, error });
};

✅
// unhandledrejection 可以捕获Promise中的错误 
window.addEventListener("unhandledrejection", function(e) {
  console.log("捕获到异常", e);
  // preventDefault阻止传播,不会在控制台打印
  e.preventDefault();
});

关于Promise的深入探讨

永远不要在 macrotask(宏任务) 队列中抛出异常,因为 macrotask 队列脱离了运行上下文环境,异常无法被当前作用域捕获。

function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
             throw Error('用户不存在')
        })
    })
}

fetch().then(result => {
    console.log('请求处理', result) // 永远不会执行
}).catch(error => {
    console.log('请求处理异常', error) // 永远不会执行
})

// 程序崩溃
// Uncaught Error: 用户不存在

不过 microtask 中抛出的异常可以被捕获,说明 microtask 队列并没有离开当前作用域,我们通过以下例子来证明:

Promise.resolve(true).then((resolve, reject)=> {
    throw Error('microtask 中的异常')
}).catch(error => {
    console.log('捕获异常', error) // 捕获异常 Error: microtask 中的异常
})

至此,Promise的异常处理需要注意的是,在macrotask级别中不要用reject,就没有抓不住的异常。

如果第三方函数在 macrotask 回调中以 throw Error 的方式抛出异常怎么办?

function thirdFunction() {
    setTimeout(() => {
        throw Error('就是任性')
    })
}

Promise.resolve(true).then((resolve, reject) => {
    thirdFunction()
}).catch(error => {
    console.log('捕获异常', error)
})

// 程序崩溃
// Uncaught Error: 就是任性

由于不在同一个调用栈,虽然这个异常无法被捕获,但也不会影响当前调用栈的执行。

必须正视这个问题,唯一的解决办法,是第三方函数不要做这种傻事,一定要在 macrotask 抛出异常的话,请改为 reject 的方式。

function thirdFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('收敛一些')
        })
    })
}

Promise.resolve(true).then((resolve, reject) => {
   // 注意一定要有这一行return,如果缺少return的话依然不能抓住这个错误
   // 因为如果没有将对方返回的promise传递下去,错误也不会传递下去
    return thirdFunction()
}).catch(error => {
    console.log('捕获异常', error) // 捕获异常 收敛一些
})

但是这样会容易忘记return,而且有多个第三方函数的时候处理的方式不太优雅

function thirdFunction() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('收敛一些')
        })
    })
}

Promise.resolve(true).then((resolve, reject) => {
    return thirdFunction().then(() => {
        return thirdFunction()
    }).then(() => {
        return thirdFunction()
    }).then(() => {
    })
}).catch(error => {
    console.log('捕获异常', error)
})

Vue 错误

main.js中添加捕获代码: 此控制台会报错,但是不会被error和onerror捕获到

window.addEventListener('error', (error) => {
  console.log('error', error);
});
window.onerror = function (msg, url, line, col, error) {
  console.log('onerror', msg, url, line, col, error);
};

可以通过 Vue.config.errorHander 来捕获异常:

Vue.config.errorHandler = (err, vm, info) => {
    console.log('捕获信息', err);
}

关于config配置捕获错误 cn.vuejs.org/api/applica…

React 错误

官方提供了 ErrorBoundary 错误边界的功能,被该组件包裹的子组件,render 函数报错时会触发离当前组件最近父组件的ErrorBoundary

react.docschina.org/docs/error-…

资源跨域

假设在当前页面引入了其他域名的JS资源,如果资源出现错误,error 事件只会监测到一个 script error 的异常。

只能捕获到 script error 的原因:

  • 有效避免敏感信息无意中被第三方(不受控制的)脚本捕获到(因此,浏览器只允许同域下的脚本捕获具体的错误信息)

解决方法:

  • 前端script加crossorigin,后端配置 Access-Control-Allow-Origin
<script src="https://www.baidu.com/index.js" crossorigin></script>

若不能修改服务端的请求头,可以考虑通过使用 try/catch 绕过,将错误抛出

 <script src="https://www.test.com/index.js"></script>
  <script>
  window.addEventListener("error", error => { 
    console.log("捕获到异常:", error);
  }, true );
  
  try {
    // 调用https://www.test.com/index.js中定义的fn方法
    fn(); 
  } catch (e) {
    throw e;
  }
   </script>

一些指标的获取

long task(执行时间超过50ms的任务,被称为 long task长任务)

const entryHandler = list => {
  for (const long of list.getEntries()) {
    // 获取长任务详情
    console.log(long);
  }
};

let observer = new PerformanceObserver(entryHandler);
observer.observe({ entryTypes: ["longtask"] });

memory页面内存

performance.memory 可以显示此刻内存占用情况,它是一个动态值,其中:

  • jsHeapSizeLimit 该属性代表的含义是:内存大小的限制。
  • totalJSHeapSize 表示总内存的大小。
  • usedJSHeapSize 表示可使用的内存的大小。

通常,usedJSHeapSize 不能大于 totalJSHeapSize,如果大于,有可能出现了内存泄漏

// load事件中获取此时页面的内存大小
window.addEventListener("load", () => {
  console.log("memory", performance.memory);
});