错误处理
前端错误处理的核心在于“兜底”。同步错误用
try-catch,异步 Promise 错误用.catch或unhandledrejection,资源加载与全局未知错误用window.addEventListener('error')捕获阶段拦截,框架层面有 React 的ErrorBoundary或 Vue 的errorHandler。
原生错误类型
JavaScript 解析或运行时发生错误,会抛出 Error 实例(包含 message 和 stack 堆栈信息)。常见衍生类型:
- SyntaxError:语法错误(如括号不匹配)。
- ReferenceError:引用错误(如使用了未声明的变量,或触发 let/const 的暂时性死区)。
- TypeError:类型错误(如对
undefined调用方法,或尝试修改const常量)。 - RangeError:范围错误(如死循环导致堆栈溢出,或数组长度被设置为负数)。
前端全局错误捕获方案
面试官经常会问:“如果线上项目白屏了,你怎么做前端异常监控和上报?”
1. 捕获常规同步/异步运行时错误
try-catch:只能捕获同步代码中的常规错误。无法捕获语法错误和异步回调错误(如setTimeout中的错误,因为执行时原来的try-catch块早已出栈销毁)。window.onerror:全局捕获运行时错误。当 JS 发生预料之外的错误时,兜底触发。
2. ⭐捕获静态资源加载错误
图片 <img src="404.png"> 或脚本加载失败时,错误不会冒泡,因此 window.onerror 根本抓不到!
- 解决方案:使用
window.addEventListener('error', handler, true),第三个参数设为true,利用事件流的捕获阶段将资源加载错误提前拦截下来。
3. ⭐捕获未处理的 Promise 错误
如果 Promise 发生 reject 且没有写 .catch(),这个错误会被默默吞掉,不会触发 window.onerror。
- 解决方案:全局监听
unhandledrejection事件。
window.addEventListener("unhandledrejection", (event) => {
console.log("捕获到未处理的 Promise 错误:", event.reason);
event.preventDefault(); // 阻止控制台飘红报错
});
4. 框架层面的错误边界 (Error Boundary)
面试坑点:既然有了
window.onerror,为什么还要搞框架的错误边界? 因为window.onerror只能“监听到”错误并上报给服务器,但它无法阻止 React/Vue 的渲染崩溃。在现代 SPA 中,如果某个子组件在渲染(Render)期间抛出错误,整个组件树就会被直接卸载,导致用户看到彻底的“白屏”。 错误边界的作用是“渲染降级”:它能把崩溃限制在局部的组件内,并展示一个备用的 UI(比如“该模块加载失败”),而不是让整个应用死掉。
现代 SPA 应用中,如果组件在渲染期间报错,会导致整个 DOM 树被卸载(也就是常说的“白屏”)。
- React:使用类组件生命周期
componentDidCatch和static getDerivedStateFromError封装错误边界组件,渲染降级 UI(如显示“服务器开小差了”)。 - Vue:在全局配置
app.config.errorHandler统一接管组件错误;或使用生命周期钩子errorCaptured局部捕获。
5. 跨域脚本错误 (Script error.)
如果引入了外链的跨域 JS 文件(如 CDN 上的脚本),它内部报错时,出于浏览器安全策略,window.onerror 只能拿到一个干瘪的 "Script error.",拿不到具体的行号和堆栈。
- 解决方案:
- 给
<script>标签增加crossorigin="anonymous"属性。 - 对应的脚本服务器响应头必须增加
Access-Control-Allow-Origin: *。
- 给
Error Cause (异常链)
在复杂的前端架构中(尤其是涉及到多层封装的 SDK、BFF 层请求或复杂的组件嵌套时),底层抛出的错误往往缺乏业务上下文。比如,一个底层的网络请求失败了,直接抛出 Failed to fetch,但在业务层,我们更想知道这个错误是因为“获取用户信息失败”导致的。
以前的做法是重新抛出一个自定义错误,但这会导致原始错误的堆栈(Stack Trace)丢失。
为了解决这个问题,ES2022 引入了 Error Cause 特性。
如何使用 Error Cause
在 new Error 时,可以传入第二个参数对象,其中 cause 属性用于保存原始错误。
async function fetchUserData() {
try {
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return await response.json();
} catch (error) {
// 捕获底层网络错误,并包装上业务上下文,同时通过 cause 保留原始堆栈
throw new Error("获取用户信息失败,请稍后重试", { cause: error });
}
}
async function renderUserPage() {
try {
await fetchUserData();
} catch (error) {
console.error(error.message); // 输出: 获取用户信息失败,请稍后重试
// 通过 error.cause 拿到引发该错误的“元凶”,并上报给监控系统
if (error.cause) {
console.error("底层原始错误:", error.cause.message);
console.error("底层堆栈:", error.cause.stack);
}
}
}
异常链对错误捕获的帮助
在前端监控和错误处理体系中,Error Cause 提供极大的帮助:
- 友好的用户提示与精准的监控告警分离:业务层可以抛出
message为“网络开小差了”的业务级异常,供 UI 层捕获展示给用户;同时,异常监控 SDK(如 Sentry)可以通过读取error.cause提取到底层真实的TypeError或NetworkError以及完整调用栈,从而帮助开发者快速定位。 - 构建清晰的异常传递链:在多层架构(例如:底层的 Fetch 封装 -> 接口 Service 层 -> 业务状态层 Store -> UI 渲染层)中,每一层都可以在
catch后利用{ cause: error }包装自己的上下文并继续抛出,最终在顶层能清晰地追踪到这个错误是怎么从底层一路传递上来的。
面试扩展:为什么 try-catch 可以捕获 await 的错误?
刚才提到 try-catch 无法捕获异步错误,但它偏偏能捕获 await 抛出的错误!
这是因为 await 语法糖的底层在 Generator 自动执行器中,把 rejected 的 Promise 转换成了 throw err 的同步抛出动作,因此它能够被包裹在外层的 try-catch 完美接住。
底层执行逻辑(伪代码演示):
// 1. 你的业务代码:
async function fetchData() {
try {
await Promise.reject("请求报错啦");
} catch (err) {
console.log("接住了:", err);
}
}
// 2. V8 引擎底层 / Babel 转译后大致逻辑:
function* fetchDataGenerator() {
try {
// yield 出去一个必定会 reject 的 Promise
yield Promise.reject("请求报错啦");
} catch (err) {
console.log("接住了:", err);
}
}
// 3. Generator 的自动执行器内部逻辑(浓缩版):
const gen = fetchDataGenerator();
const p = gen.next().value; // 拿到 yield 出来的 Promise
p.catch((error) => {
// 核心机制在这里!执行器捕获到 Promise 失败后,
// 调用了 generator 的 throw 方法,把异步错误强行"塞回"了生成器内部!
// 此时生成器内部的 try-catch 就会立刻捕获到这个同步抛出的错误。
gen.throw(error);
});
面试延伸:白屏监控怎么做?
面试原题:"线上用户反馈页面白屏了,你作为前端,怎么主动发现并监控这种情况?"
难点在于:JS 都崩了,你还怎么上报?——所以必须把检测逻辑放在最早期、脱离业务代码的稳固环境里。
1. 白屏的三大根因
| 根因 | 触发场景 | 怎么发现 |
|---|---|---|
| JS 运行时报错 | 组件渲染抛错、SPA 整个组件树卸载 | ErrorBoundary / window.onerror 兜底 |
| 关键资源加载失败 | CDN 挂了、JS/CSS 404 导致页面无法初始化 | addEventListener('error', ..., true) 捕获 |
| 首屏接口失败 | 核心数据报错,导致页面渲染空状态 | 接口层 try-catch 拦截上报 |
2. 工业界标配:采样点法 (如阿里 ARMS)
为什么不直接判断 DOM 节点数量? 现代前端框架在白屏时,DOM 树往往还残留着
<div id="app"></div>甚至一些骨架屏代码。如果直接查document.body.children.length === 0极易漏报。
核心原理:
在屏幕的垂直和水平中线上,选取若干个交叉关键点(通常为 9 个)。利用原生 API document.elementsFromPoint(x, y) 获取这些坐标点最上层的 DOM 元素。如果这些元素全是基础外层容器(如 html, body, #app),说明根本没有渲染出实际的业务内容,判定为白屏。
// 1. 定义"白名单容器":如果采样点上最外层只有这些元素,说明是空的
const wrapperElements = ["html", "body", "#app", "#root"];
// 2. 检测逻辑:延时触发(避开初始化加载)或在 MutationObserver 长期不活动时触发
function checkWhiteScreen() {
let emptyPoints = 0;
const { innerWidth, innerHeight } = window;
// 选取十字交叉线上的 9 个点
const points = [
{ x: innerWidth / 2, y: innerHeight / 2 }, // 中心点
// ... 此处省略其他 8 个点坐标(如 1/4, 3/4 处)
];
points.forEach(({ x, y }) => {
// 获取坐标点从外到内的所有 DOM 元素
const elements = document.elementsFromPoint(x, y);
if (!elements.length) return;
const topElement = elements[0];
// 提取标识(有 id 用 id,没有用标签名)
const elSelector = topElement.id
? `#${topElement.id}`
: topElement.nodeName.toLowerCase();
// 如果最顶层 DOM 依然是基础容器,说明该点空白
if (wrapperElements.includes(elSelector)) {
emptyPoints++;
}
});
// 9 个点有 8 个以上是空白容器,判定为白屏
if (emptyPoints >= 8) {
reportWhiteScreen();
}
}
// 3. 上报机制
function reportWhiteScreen() {
// 必须使用 sendBeacon,保证页面关闭/崩溃时数据进入底层队列,不丢失
navigator.sendBeacon(
"/api/monitor/white-screen",
JSON.stringify({ type: "white_screen" }),
);
}
3. 30 秒面试回答模板
"白屏监控的核心是检测与上报。 检测方面,业界主流(如阿里 ARMS)采用采样点法:通过
document.elementsFromPoint获取屏幕十字交叉线上的 9 个关键点 DOM 元素,如果这些点位最顶层的元素都是html、body或#app等外层基础容器,说明业务组件根本没渲染出来,判定为白屏。 上报方面,考虑到白屏时用户往往会暴躁地刷新或关页面,统一使用navigator.sendBeacon进行异步非阻塞上报,防止请求被浏览器 cancel。 在根因防范上,我会配合ErrorBoundary兜底 JS 错误,用捕获阶段的error事件兜底静态资源失败,从而在白屏发生前提前告警。"
4. 高频追问
- Q:什么时候触发检测?只在 onload 吗?
- A:不能只靠
onload。通常结合MutationObserver监听 DOM 变化,如果一段时间(如 3s)DOM 不再发生变化,开始执行采样点检测。SPA 路由切换时也要重置并重新检测。
- A:不能只靠
- Q:怎么区分"白屏"和"长时间的骨架屏"?
- A:如果业务里有骨架屏,要把骨架屏的特定 class(如
.skeleton-wrapper)也加入上面的wrapperElements容器白名单中。只要长时间没被真实数据替换掉,采样点获取到的永远是骨架屏,同样视为白屏异常。
- A:如果业务里有骨架屏,要把骨架屏的特定 class(如
- Q:为什么不用
fetch上报?- A:白屏发生时,用户往往会立刻刷新或关掉页面。
fetch请求此时会被浏览器强行 Cancel 导致上报失败,而sendBeacon是浏览器底层队列处理,关页面也能发出去。
- A:白屏发生时,用户往往会立刻刷新或关掉页面。
- Q:如果连
sendBeacon都不能用,还有别的兜底方案吗?- A:有。核心思路是避开"页面销毁"这个时间窗口或绕过 HTTP 协议限制,常见三种:
new Image().src = url:动态创建<img>,把上报数据拼在 URL query 里。<img>的请求不受页面卸载影响,老 IE 时代最常用的兜底方案。缺点:只能 GET、URL 长度受限、不能拿到服务端响应。- 页面卸载前
beforeunload同步阻塞上报:监听window.addEventListener('beforeunload', ...),在里面用同步的XMLHttpRequest.send()发起请求。浏览器会延迟关闭页面几百毫秒等请求完成。⚠️ 但是现代浏览器对这种"阻塞关页面"行为做了大量限制(Chrome 会忽略beforeunload的 returnValue 提示,且对unload监听中的同步请求也会被砍掉),所以仅作为兜底,不能依赖。 - 本地暂存 + 下次启动补传:上报前先写入
localStorage或 IndexedDB(同步、必成功),后台静默排队;下次页面正常打开时再统一补传到服务器。✅ 最稳的兜底方案——即使本次上报失败,下一次访问也能把丢的数据补回来。
- A:有。核心思路是避开"页面销毁"这个时间窗口或绕过 HTTP 协议限制,常见三种: