前端监控基本原理 - 错误捕获介绍

874 阅读4分钟

作者:王特

前言

异常与错误是我们开发,甚至线上难免会遇到的问题,触发的原因可能是各种各样的。但是即使运行时页面中偶现错误,我们也需要保证页面的稳定性,并且因此错误的捕获尤为重要,此外,线上监控能够发现问题、排查问题的前提也是代码中的错误被正确的捕获并且上报。本文尽可能详细的介绍了可能存在的错误与捕获方式。

前端错误分类

错误分类解决方案错误示例
运行错误错误捕获(下面详细探讨)
资源加载错误错误捕获window.addEventListener('error', event => {});
Ajax 请求错误重写错误处理运行错误 + http 错误 + 自定义错误
网页崩溃Service worker线程心跳--

我们重点探讨下运行错误

运行错误的影响

function foo() {
    console.log('foo function work 1');
    setTimeout(() => {
        console.log('some async work');        
    });
    throw new Error('setTimeout error');
    console.log('foo function work 2');    
    console.log('foo function work 3');    
    console.log('foo function work 4');        
}

console.log('global work 1');
foo();
console.log('global work 2');
console.log('global work 3');

运行结果:

影响:

运行错误,只影响同步代码的执行,异步代码无影响。try catch的作用可以进一步缩小错误影响范围。

错误捕获方式

我们从几个例子逐步来探讨

异步任务错误捕获

代码片段一

try {
  setTimeout(() => {
    throw new Error('setTimeout error');
  });
  throw new Error('try block error');
} catch (error) {
  console.log('Caught', error);
}

运行结果

相关拓展

try {
    window.requestAnimationFrame(() => throw new Error('some error'))
} catch(error) {
    console.log(error);
}

结论

  1. 异步任务无法使用try catch捕获。

捕获兜底

// window 全局检测错误,非捕获
window.onerror = (error) => {
  console.log('Window Onerror:', error);
}
window.addEvenListener('error', e => {}, true)

Promise 错误捕获

Promise 语法结构中,有同步执行的部分,也有异步执行的部分,接下来我们来研究下这两部分的执行效果。

代码片段一

try {
  new Promise((resolve, reject) => {
    throw new Error('promise error');
  });
  throw new Error('try block error');
} catch (error) {
  console.log('Caught', error);
}

运行结果

相关拓展

try {
  Promise.resolve().then(() => {
  thw new Error    omise th    rror');
  })
} catch(error) {
  console.log('Caught', error);
}

结论

  1. Promise 内部有自建的错误处理机制,Promise相关的同步和异步代码错误,都走自建的错误处理。

捕获兜底

window.addEventListener('unhandledrejection', event => {
  /* 你可以在这里添加一些代码,以便检查
     event.promise 中的 promise 和
     event.reason 中的 rejection 原因 */
  console.log('unhandledrejection', event);
  event.preventDefault();
}, false);

window.addEventListener("rejectionhandled", event => {
  console.log("Promise rejected; reason: " + event.reason);
}, false);

Generator 错误捕获

代码片段一

function* generator() {
  console.log('generator begin');
  yield 1;
  throw new Error('generator error');
  console.log('generator second step');
  yield 2;
  console.log('generator third step');
}
(function(gen) {
  try {
    console.log(gen.next());
    console.log(gen.next());
    console.log(gen.next());
  } catch (error) {
    console.log('Caught', error);
  }
})(generator());

运行结果:

代码片段二

function* generator() {
  try {
    console.log('generator begin');
    yield 1;
    throw new Error('generator error');
    console.log('generator second step');
    yield 2;
    console.log('generator third step');
  } catch (error) {
    console.log('Caught Error:', error);
  }
}
(function(gen) {
  for (let i = 0; i < 3; i++) {
    setTimeoout(() => gen.next(), i * 1000)
  }
})(generator());

Async 错误捕获

代码片段一

async function asyncFun() {
  throw new Error('async error');
}
(async function () {
  try {
    await asyncFun();
    throw new Error('try block error');
  } catch (error) {
    console.log('Caught', error);
  }
})();

运行结果

相关拓展

async function asyncFun() {
  throw new Error('async error');
}
(async function () {
  try {
    asyncFun();
    throw new Error('try block error');
  } catch (error) {
    console.log('Caught', error);
  }
})();

结论

  1. Async await 是基于Promise、Generator的语法糖,其错误处理遵循Promise的处理方式;
  1. await 有将Promise错误转成同步错误的作用。

React 错误捕获

代码片段一

function ChildNode() {
  throw new Error('ChildNode render error');
  return <span>This is a child node.</span>
}
function ParentNode() {
  try {
    return <div><ChildNode/></div>;
  } catch (error) {
    return <div>Render Error</div>;
  }
}

代码片段二

function ChildNode() {
  throw new Error('ChildNode render error');
  return <span>This is a child node.</span>
}
class ParentNode extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    logErrorToMyService(error, errorInfo);  
  }
  render() {
    if (this.state.hasError) {  
      return <div>Render Error</div>;    
    }
    return <div><ChildNode/></div>;
  }
}

探索原理

  1. React 的错误捕获基于 Fiber 树,理解了 Fiber,就很容易理解 React 的错误捕获。错误捕获范围仅限于 Fiber 的执行内容,不包含以下内容:

源码路径:packages/react-reconciler/src/ReactFiberThrow.new.js

  1. React 错误边界,是检测,而非捕获。即使有边界,错误同样也会被抛出,参考下图官方示例:

总结

运行错误类型

错误类型简介错误示例
EvalErroreval 函数中发生的错误根据EcmaSpec 2018版: 此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性。
InternalError内部错误该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。示例场景通常为某些成分过大,例如:“too many switch cases”(过多case子句);“too many parentheses in regular expression”(正则表达式中括号过多);“array initializer too large”(数组初始化器过大);
RangeError数值范围错误
ReferenceError引用错误
SyntaxError语法错误
TypeError类型错误
URIError全局URI处理函数错误

运行错误处理分类总结

分类总结涉及错误解决方案
同步错误同步错误try catchwindow.onerrorwindow.addEvenListener('error', e => {}, true)
异步错误除Promise外的其他异步错误window.onerrorwindow.addEvenListener('error', e => {}, true)
Promisewindow.addEvenListener('unhandledrejection', e => {}, true)window.addEvenListener('rejectionhandled', e => {}, true)

运行错误处理方式总结

错误捕获方式 特点
try catch - 局部
window.onerror - 全局
  • 可以阻止冒泡

  • 不能捕获资源加载错误

  • 错误信息比较全:

    • message
    • source 源代码路径
    • lineno 行号
    • colno 列号
    • error 错误堆栈
window.addEvenListener('error', e => {}, true) - 全局
  • 不能阻止冒泡
  • 可以捕获资源加载错误
  • 能拿到DOM节点
window.addEvenListener('unhandledrejection', e => {}, true) - 全局
  • Promise错误捕获异步
window.addEvenListener('rejectionhandled', e => {}, true) - 全局
  • Promise错误捕获异步

参考文档

javascript.info/promise-err…

juejin.im/post/5c1dd6…

juejin.im/post/5c1998…

es6.ruanyifeng.com/?from=from_…

juejin.im/post/5e78d5…

jakearchibald.com/2015/tasks-…

en.wikipedia.org/wiki/Corout…

www.jianshu.com/p/3b2f8e3af…

blog.csdn.net/m0_49016709…