所有人都需要的前端报错机制知识库

74 阅读6分钟

为啥要处理异常?

异常不可控,会影响最终的呈现效果,对异常的处理,主要作用有:

  • 增加用户体验
  • 远程定位问题
  • 绸缪,及早发现问题
  • 完善前端方案,前端监控系统

异常捕获的类型有以下7种:

  • JS语法错误、代码异常
  • AJAX请求异常
  • 静态资源加载异常
  • Promise异常
  • Iframe异常
  • 跨域Script error
  • 崩溃和卡顿

而捕获的js类型错误也分为7种:

  • Error ★ (很少见,基本都是开发人员自定义的)
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError(语法错误)★
  • TypeError ★ (常见,类型不对)
  • URIError (很少见,仅在encodeURI和decodeURI方法中见到)

处理JS报错的常用方法?

try-catch-finally

  • try-catch只能捕获到同步的运行时错误,对语法和异步错误缺捕获不到
    try {
      let name = 'jartto';
      console.log(nam);
    } catch(e) {
      console.log('捕获到异常:',e);
    }

    输出
    捕获到异常:ReferenceError: nam is not defined at <anonymous>:3:15
  • 不能捕获到具体的语法错误,只有一个语法提示,对上一个例子改造一下,删除一个单引号
    try {
      let name = 'jartto;
      console.log(nam);
    } catch(e) {
      console.log('捕获到异常:',e);
    }

    输出
    捕获到异常:Uncaught SyntaxError: Invalid or unexpected token
  • 不能捕获异步代码中的错误
    try {
      setTimeout(() => {
        undefined.map(v => v);
      }, 1000)
    } catch(e) {
      console.log('捕获到异常:',e);
    }

    输出
    Uncaught TypeError: Cannot read property 'map'ofundefined at setTimeout (<anonymous>:3:11)

window.onerror

当JS运行时错误发生,window会触发一个ErrorEvent 接口的error事件,并执行window.onerror(),使用语法为:

    /**
    * @param {String}  message    错误信息
    * @param {String}  source    出错文件
    * @param {Number}  lineno    行号
    * @param {Number}  colno    列号
    * @param {Object}  error  Error对象(对象)
    */

    window.onerror = function(message, source, lineno, colno, error) {
       console.log('捕获到异常:',{message, source, lineno, colno, error});
    }
  • 捕获同步运行的报错
   Jartto;

   捕获到异常: 
   { message:"Uncaught ReferenceError:Jartto is not defined",
     source:"http://127.0.0.1:8001/",
     lineno:35,
     colno:1
   }
  • 捕获语法错误,不能捕获到具体信息
    let name = 'Jartto;

    捕获到异常: 
    Uncaught SyntaxError: Invalid or unexpected token
  • 异步运行错误
    setTimeout(() => {
        Jartto;
    });

    捕获到异常:
    {
       message: "Uncaught ReferenceError: Jartto is not defined", 
       source: "http://127.0.0.1:8001/", 
       lineno: 36, 
       colno: 5, 
       error: ReferenceError: Jartto is not defined at setTimeout (http://127.0.0.1:8001/:36:5)
    }
  • 网络请求 - 捕获不到
    <img src="./jartto.png">

使用总结:不论是静态资源异常,或者接口异常,错误都无法捕获到。

在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。

window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的onerror() 处理函数。这些 error 事件不会向上冒泡到 window ,能被单一的window.addEventListener 捕获,但是不同浏览器对于error事件的处理略有不同,需要特殊处理。

<scritp>
window.addEventListener('error', (error) => {
    console.log('捕获到异常:', error);
}, true)
</script>

<img src="./jartto.png">

捕获到异常:
Event {isTrusted: true, type: 'error', target: img, currentTarget: window,...}

使用该方法需要注意:

  • 不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。
  • 需要注意避免 addEventListener 重复监听。

unhandledreject捕获Promise异常

现有项目中Promise基本上都用 async...await写法了,既然async...await使代码编写的更像是同步代码,当然可是可以使用try...catch这种方是去处理异常的,写个示例如下:

async function fetchData() { 
    try { 
        const response = await fetch('https://api.example.com/data'); 
        if (!response.ok) { 
            throw new Error(`HTTP error! status: ${response.status}`); 
        } 
        const data = await response.json(); 
        console.log(data); 
     } catch (error) { 
         // 这里捕获并处理所有在 'await' 语句中产生的错误 
         console.error('Error fetching data:', error.message); 
     } 
} 
fetchData();

但是对于于原生的Promise写法try...catch则就失效了,try...catch 是一种同步的错误处理机制。它允许你在一段代码块(try 块)中捕获执行时可能发生的错误。如果在 try 块中抛出了异常,控制权将立即转移到相应的 catch 块,其中可以处理或记录错误。如果没有 catch 块处理异常,或者异常在 catch 块中未被捕获,则程序会终止。

try { 
     // 可能抛出错误的代码 throw new Error("Something went wrong");
} catch (error) { 
    // 错误处理代码 console.error(error.message); 
}

Promise 是用于处理异步操作的。当一个 Promise 成功(resolved)或失败(rejected)时,你可以使用 .then().catch() 方法来处理结果或错误。.then() 方法通常用于处理成功的情况,而 .catch() 用于处理失败的情况。

new Promise((resolve, reject) => { 
    // 异步操作 
    setTimeout(() => { 
        reject(new Error("Async operation failed")); 
    }, 1000); 
}).then(result => { 
    // 成功时的处理 
    console.log(result); 
}).catch(error => { 
    // 失败时的处理 
    console.error(error.message); 
});

总结:try...catch 语句本身不能直接捕获由 Promise 链中产生的错误,这是因为 Promise 的异步特性。Promise.then().catch() 方法是在 Promise 结果可用或拒绝时异步调用的,而 try...catch 只能捕获同步代码中的错误。

但是,你可以通过将 Promise 的创建和调用放在 try...catch 块内,来捕获那些在 Promise 构造函数中抛出的错误,或者在 then 方法中同步抛出的错误。然而,这种方式并不能捕获在 Promise 内部异步执行时抛出的错误,这些错误应该在 .catch() 中处理。

但是如果遇到了一个老项目,大部分都是缺少catch的残缺代码,如下:

newPromise((resolve, reject) => {  
    reject('jartto: promise error');  
});

此时为项目中每一处都加上catch则不太现实,这个时候,unhandledreject就派上用场了,即使项目中Promise全部加上了catch处理,为了防止有漏掉的Promise异常,也建议增加一个全局的对unhandledreject监听,

window.addEventListener("unhandledrejection", function(e){
  e.preventDefault()
  console.log('捕获到异常:', e);
  return true;
});
Promise.reject('promise error');

备注:e.preventDefault()可阻止异常消息在控制台上的显示

参考文章:

使用vue+node搭建前端异常监控系统

react异常捕获

一个非常优秀的库:react-error-boundary(会被最近的错误边界组件捕获)

核心原理:采用react v16版本后自带的error boundary,通过静态方法getDerivedStateFromError和生命周期componentDidCatch方法完成错误的捕获和上报

不足之处:仅能捕获子组件在渲染期间的同步错误

无法捕获:父组件本身的错误、异步错误、各种合成事件中的错误、服务端渲染的错误、函数式组件无法定义Error Boundary,不过可以使用

测试用例:act

  • react事件回调中发生的错误无法被ErrorBoundary捕获的原因

    事件回调并不属于react工作流程:render阶段,即「组件render」、「Diff算法」发生的阶段,commit阶段,即「渲染DOM」、「componentDidMount/Update执行」阶段

参考文章:

造一个 react-error-boundary 轮子

为什么Hook没有ErrorBoundary?

vue异常捕获

在vue中,没有直接与React的Error Boundary对标的组件处理方法,但是提供了两种钩子函数errorCapturedonErrorCaptured,用来捕获渲染过程中的错误,并且可以作为错误边界来防止一个组件的错误导致整个应用的崩溃。

errorCaptured

errorCaptured 钩子函数可以在任何组件中定义,它接收三个参数:

  • err - 发生的错误对象。
  • vm - 发生错误的 Vue 实例。
  • info - 错误的来源信息,比如是来自生命周期钩子还是异步更新队列。
export default { 
    name: 'App', 
    errorCaptured(err, vm, info) { 
        console.error(`An error occurred during ${info}: `, err); 
        // 返回 false 以让错误继续传播 
        return false; 
    }, 
}

该钩子函数应该返回一个布尔值。如果返回 true,则表示错误已被处理,不会再向上抛出。如果返回 false 或者抛出一个新的错误,则会继续向上传播错误。

onErrorCaptured

onErrorCapturederrorCaptured类似,使用方法也基本一致,该方法由vue3引入,使用方法如下:

import { onMounted, onErrorCaptured } from 'vue'; 

export default { 
    setup() { 
        onMounted(() => { 
            // 组件挂载后的操作 
        }); 
        
        onErrorCaptured((err, instance, info) => { 
            console.error(`An error occurred during ${info}: `, err); 
            return false; 
        }); 
    }
}

vue还提供了另外一种处理运行时错误的方法:errorHandler,该方法和上述两种方法有着明显的区别

errorHandler

  • errorHandler

errorHandler是一个全局的错误处理器,可以通过 Vue 的配置对象来设置。当 Vue 应用中的任何地方出现错误时,无论是在组件内还是在其他地方,errorHandler 都会被调用。这意味着它可以捕获所有类型的错误,包括那些在组件的生命周期钩子、异步更新队列、watcher、计算属性或模板渲染过程中发生的错误。使用方法如下:

Vue.config.errorHandler = function (err, vm, info) { 
    // err 是错误对象 
    // vm 是发生错误的 Vue 实例 
    // info 是错误的来源信息 
    console.error('Global error handler:', err, info); };
  • errorCaptured

errorCaptured 是一个组件级别的生命周期钩子,只能在组件内部定义。它会在组件渲染和观察期间捕获子组件树内的错误。当一个子组件或其子树中的组件抛出错误时,错误会被最近的具有 errorCaptured 钩子的祖先组件捕获。如果当前组件没有处理错误,错误将继续向上冒泡,直到被更高层级的 errorCaptured 处理,或者最终被全局的 errorHandler 处理。

export default { 
    errorCaptured: function (err, vm, info) { 
        // err 是错误对象 
        // vm 是发生错误的 Vue 实例 
        // info 是错误的来源信息 
        console.error('Component error captured:', err, info); 
        // 返回 true 表示错误被处理了,不会向上冒泡 
        return true; 
    } 
};
  • onErrorCaptured
import { onErrorCaptured } from 'vue'; 
export default { 
    setup() { 
        onErrorCaptured((err, instance, info) => { 
            // err 是错误对象 
            // instance 是发生错误的 Vue 实例 
            // info 是错误的来源信息 
            console.error('Component error captured:', err, info); 
            // 返回 true 表示错误被处理了,不会向上冒泡 
            return true; 
        }); 
     } 
};

主要区别

  • 作用范围

    • errorHandler 是全局的,可以捕获应用中的所有错误。
    • errorCaptured 是局部的,仅捕获其子组件树中的错误。
  • 错误处理

    • errorHandler 不会阻止错误冒泡,一旦发生错误,它总是会被调用。
    • errorCaptured 允许组件处理错误并在必要时阻止错误冒泡。
  • 调用顺序:

    • 当一个错误发生时,errorCaptured 首先被调用,如果错误没有被处理(即 errorCaptured 返回 false 或抛出新的错误),那么错误会继续传播,最终到达 errorHandler

总之,errorHandler 更适合于全局错误处理和日志记录,而 errorCaptured 则更适合于局部错误处理和错误恢复逻辑。在构建复杂的应用时,两者结合使用可以提供更全面的错误管理策略。

监控平台的开发和接入

参考文章:

使用vue+node搭建前端异常监控系统

# Sentry原理--收集错误、上报