错误处理
前端错误处理的核心在于“兜底”。同步错误用
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);
});