捕获错误 🔍 前端运行报错捕获技术方案

521 阅读8分钟

4.png

一、前言

在现代前端应用的开发过程中,确保页面的稳定运行和良好的用户体验至关重要。然而,由于各种原因,页面在运行过程中可能会出现各种错误,如资源加载失败、JavaScript 代码执行错误、API 请求异常等。这些错误不仅会导致页面功能异常,还可能引发页面崩溃或白屏,严重影响用户的使用体验。因此,有效地捕获和处理页面运行时的错误是前端开发者必须面对的一项重要任务。本文将深入探讨几种常见的前端页面运行报错捕获技术方案,涵盖代码逻辑错误、资源加载错误和接口请求错误等不同场景的捕获方法,并区分全局捕获与局部捕获等策略。通过对这些技术方案的详细研究,获取报错数据将使我们能够及时且有效地发现问题和解决问题,从而显著提升用户的使用体验和满意度。

二、报错分类

分类说明
同步代码出错这类错误一般发生在脚本执行的过程中,通常会导致 JS 执行中断,例如访问未定义的变量、调用代码中不存在的方法、代码拼写或语法错误等。
异步代码出错这类错误发生在异步操作中,如定时器回调函数中抛出的错误、事件监听处理函数中抛出的错误、Promise/reject、async/await 函数报错等。
资源加载出错这类错误发生在页面资源加载过程中,如图片、CSS 文件、JavaScript 文件等资源文件路径不存在、请求存在跨域问题、资源类型不支持等场景。
接口请求报错这类错误发生在与服务器进行数据交互时,例如请求存在跨域问题、网络断开、请求超时、服务器处理异常(500)、请求内容不存在(404)等场景。

三、报错捕获

3.1 同/异步代码出错捕获

截屏2025-01-11 21.42.47.png

3.1.1 同步代码

同步代码的出错我们可以通过 try catch局部进行捕获,也可以通过window.onerrorwindow.addEventListener("error")全局捕获未在局部捕获的错误,但是 window.onerror 是一个全局变量,存在后续被覆盖重写的可能性。

window.onerror = function (msg, url, line, col, error) {
  console.log("【catch】window.onerror: ", msg, url, line, col, error);
};

window.addEventListener("error", function (event) {
  console.log("【catch】window.addEventListener: ", event);
});

try {
  throw new Error("出错啦!1");
} catch (e) {
  console.log("【catch】try catch: ", e);
}

throw new Error("出错啦!2");

image001.png

3.1.2 异步代码

定时器回调和事件监听处理回调等异步代码报错无法通过 try catch局部进行捕获,但可以通过 window.onerrorwindow.addEventListener("error")全局捕获。

window.onerror = function (msg, url, line, col, error) {
  console.log("【catch】window.onerror: ", msg, url, line, col, error);
};

window.addEventListener("error", function (event) {
  console.log("【catch】window.addEventListener.error: ", event);
});

setTimeout(() => {
  throw new Error("出错啦!1");
});

try {
  setTimeout(() => {
    throw new Error("出错啦!2");
  });
} catch (e) {
  console.log("【catch】try-catch: ", e);
}

image003.png

Promise 异步代码内部有自建的错误处理机制,无法通过 try catch 在局部进行捕获,只能通过 Promise.catchPromise.then局部进行捕获。未在局部进行捕获处理的错误,可以通过 window.addEventListener("unhandledrejection")全局捕获。

window.addEventListener("unhandledrejection", function (event) {
  console.log("【catch】window.addEventListener.unhandledrejection.window", event);
});

try {
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("出错啦!1"));
    }, 1000);
  });
} catch (e) {
  console.log("【catch】try-catch: ", e);
}

new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("出错啦!2"));
  }, 1000);
});

new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("出错啦!3"));
  }, 1000);
}).catch((e) => {
  console.log("【catch】Promise.catch: ", e);
});

new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("出错啦!4"));
  }, 1000);
}).then(
  (e) => {},
  (e) => {
    console.log("【catch】Promise.then: ", e);
  }
);

截屏2025-02-04 20.34.53.png

await 有将 Promise 错误转成同步错误的作用,可以通过 try catch局部进行捕获,未在局部进行捕获处理的错误,也可以通过 window.addEventListener("unhandledrejection")全局捕获。注意,一个函数只要加了async,即使内部是同步逻辑,也需要按照异步执行。

window.addEventListener("unhandledrejection", function (event) {
  console.log("【catch】window.addEventListener.unhandledrejection.window", event);
});

async function asyncFunc() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("出错啦!"));
    }, 1000);
  });
}

async function test1() {
  await asyncFunc();
}
test1();

async function test2() {
  try {
    await asyncFunc();
  } catch (e) {
    console.log("【catch】try-catch: ", e);
  }
}
test2();

async function asyncFunc2() {
  throw new Error("error");
}
async function test3() {
  asyncFunc2().catch((res) => {
    console.log("【catch】catch: ", res);
  });
}
test3();

image007.png

rejectionhandled 事件在某些场景也可以捕获错误,当一个 Promise 最初被拒绝且没有拒绝处理器,但在稍后的时间(通常是一个事件循环之后)获得了拒绝处理器时,会触发 rejectionhandled 事件。

// 监听 unhandledrejection 事件
window.addEventListener("unhandledrejection", function (event) {
  console.log("Unhandled rejection:", event.reason);
});

// 监听 rejectionhandled 事件
window.addEventListener("rejectionhandled", function (event) {
  console.log("Rejection handled:", event.promise);
});

// 创建一个被拒绝的 Promise
const promise = Promise.reject(new Error("Something went wrong"));

// 延迟添加拒绝处理器
setTimeout(() => {
  promise.catch((error) => {
    console.log("Caught error:", error);
  });
}, 1000);

image009.png

One more thing: error-stack-parser 可以解析 JS 错误堆栈信息,获取引发错误的函数名称、函数定义所在的文件名、错误发生的行号/列号和原始的堆栈轨迹字符串等。

3.2 资源加载出错捕获

加载不存在的图片、JS 文件以及 CSS 文件可以通过 window.addEventListener("error") 在全局捕获,也可以通过局部的 XXX.onErrorXXX.addEventListener("error") 在局部进行捕获。

<script>
  window.addEventListener("error", function (event) {
    console.log("【catch】window.addEventListener.error: ", event);
  });
</script>

<script src="invalidJs.js"></script>
<img src="invalidImage.png" />
<link rel="stylesheet" type="text/css" href="invalidCss.css" />

image011.png

<img src="invalidImage.png" id="image" />
<img src="invalidImage.png" onerror="console.log('【catch】img.onerror: ', event);" />

<script>
  const script = document.createElement("script");
  script.src = "invalidJs.js";
  script.onerror = function (event) {
    console.log("【catch】script.onerror: ", event);
  };
  document.head.appendChild(script);

  const img = document.getElementById("image");
  img.addEventListener("error", function () {
    console.log("【catch】img.addEventListener.error: ", event);
  });
</script>

image013.png

3.3 接口请求报错捕获

注意,这里所谓的接口请求报错指的是“服务端返回非 200 状态码或请求函数报错”的场景,服务端返回数据异常导致报错归类为代码逻辑出错场景。

3.3.1 fetch 请求

fetch 本质上是返回 Promise,因此可以通过 Promise.catchPromise.then局部进行捕获,如未在局部捕获,则可以通过 window.addEventListener("unhandledrejection")全局捕获。

window.addEventListener("unhandledrejection", function (event) {
  console.log("【catch】window.addEventListener.unhandledrejection.window", event);
});

fetch("https://www.invalid.com");

fetch("https://www.invalid.com").catch((res) => {
  console.log("【catch】catch: ", res);
});

image015.png

3.3.2 xhr 请求

xhr 请求可以监听 error 事件来进行局部错误捕获,XMLHttpRequest 的错误默认不会触发全局的 error 事件,因此需要在局部捕获错误时手动抛出异常,以便全局捕获。

const xhr = new XMLHttpRequest();
xhr.open("GET", "localhost:8080", true);
xhr.onerror = function () {
  // 这里抛出错误,则全局可以捕获
  console.log("【catch】xhr.onerror: ", event);
};
xhr.send();

image017.png

3.3.3 axios 库

Axios 是一个基于 promise 网络请求库,其被广泛应用作为前端请求库使用,在 axios 中我们可以在局部通过 catch 进行报错捕获,也可以通过拦截器统一设置全局报错捕获。

axios.get('/user/12345')
  .catch(function (error) {
    if (error.response) {
      // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // 请求已经成功发起,但没有收到响应
      // `error.request` 在浏览器中是 XMLHttpRequest 的实例,
      // 而在node.js中是 http.ClientRequest 的实例
      console.log(error.request);
    } else {
      // 发送请求时出了点问题
      console.log('Error', error.message);
    }
    console.log(error.config);
  });
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

3.4 Vue 运行报错捕获

Vue 提供了app.config.errorHandler 用于为应用内抛出的未捕获错误指定一个全局处理函数。它可以从下面这些来源中捕获错误:

  • 组件渲染器
  • 事件处理器
  • 生命周期钩子
  • setup() 函数
  • 侦听器
  • 自定义指令钩子
  • 过渡 (Transition) 钩子
app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
}

3.5 React 运行报错捕获

React 可以使用 react-error-boundary 捕获组件渲染时抛出的错误,注意它不会捕获异步代码和事件处理程序中的错误,推荐阅读 👉 如何正确的捕获React中的错误。除此之外,也可以使用 componentDidCatch 这一生命周期方法用于捕获子组件树中抛出的 JavaScript 错误。

import { ErrorBoundary } from "react-error-boundary";

const logError = (error: Error, info: { componentStack: string }) => {
  // Do something with the error, e.g. log to an external API
};

<ErrorBoundary fallback={<div>Something went wrong</div>} onError={logError}>
  <ExampleApplication />
</ErrorBoundary>

四、总结

在本文中,我们深入探讨了前端报错捕获的多种技术方案,从全局错误监听器的设置到异步错误的处理,再到资源加载和接口请求错误的捕获,全方位地覆盖了前端应用中可能出现的各种错误场景。通过这些技术手段,我们能够在及时地收集和上报系统的错误信息,为项目质量提供了有力的数据支持。然而,在实际应用中页面报错监控仅能作为技术层面的观测指标,而无法直接等同于用户侧的稳定性体验指标。在实际场景中,我们常常会发现控制台出现大量报错,然而这些报错却并未对系统的正常运行造成实质性影响。因此,我们更多地是将页面报错作为关注页面白屏、崩溃以及交互异常等关键用户体验指标的辅助指标,而非单纯以报错数量来衡量系统的稳定性。

「前端监控专栏」更多内容 👇