1. 前端错误处理:从崩溃到优雅降级的实战指南
凌晨三点的办公室,咖啡已经凉透,屏幕上闪烁的红色错误提示像是一张嘲笑的脸。Cannot read properties of undefined——这个看似简单的错误已经折磨了你两个小时。这不是虚构的场景,而是每个前端开发者都经历过的真实噩梦。
错误处理不是锦上添花的功能,而是现代前端开发的生存技能。根据Sentry的年度报告,超过60%的用户会在遇到页面错误后直接离开,而良好的错误处理可以将用户留存率提升3倍以上。
1.1 为什么前端错误处理如此重要?
想象一下这样的场景:用户正在你的电商网站结账,点击"支付"按钮后页面突然白屏。没有提示,没有解释,只有一片空白。这种情况下,用户不仅会放弃当前交易,很可能永远不会再回来。
前端错误处理的本质是防御性编程的实践。与后端不同,前端运行在不可控的环境中:
- 用户可能使用各种奇怪的浏览器和设备
- 网络连接可能随时中断
- 第三方脚本可能意外崩溃
- API返回的数据结构可能与预期不符
良好的错误处理系统就像飞机的黑匣子+安全气囊组合:既能记录问题发生时的完整上下文,又能在崩溃时保护用户体验不彻底崩溃。
2. 全局错误捕获:构建你的安全网
2.1 window.onerror:最后的防线
window.onerror是JavaScript最基础的错误捕获机制,可以捕获大多数同步错误。但它的API设计相当反直觉:
javascript复制window.onerror = function(message, source, lineno, colno, error) {
// message: 错误信息字符串
// source: 发生错误的脚本URL
// lineno: 行号
// colno: 列号
// error: Error对象(包含堆栈信息)
// 返回true可以阻止错误在控制台显示
return false;
};
实际项目中,我们需要更健壮的处理:
javascript复制window.onerror = function(msg, url, line, col, error) {
// 过滤浏览器插件错误
if (url?.includes('chrome-extension')) return true;
const errorInfo = {
type: 'javascript',
message: msg,
filename: url,
position: `${line}:${col}`,
stack: error?.stack,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
};
// 上报到监控系统
reportError(errorInfo);
return false; // 开发环境保持控制台可见
};
常见陷阱:
- 跨域脚本错误只会显示"Script error",需要添加
crossorigin属性 - 某些浏览器对colno的支持不一致
- 无法捕获资源加载失败(如图片、CSS)
2.2 unhandledrejection:Promise错误的救星
随着async/await的普及,Promise rejection成为前端主要错误来源之一。全局捕获方案:
javascript复制window.addEventListener('unhandledrejection', event => {
const reason = event.reason;
let message = 'Unhandled Promise Rejection';
let stack = '';
if (reason instanceof Error) {
message = reason.message;
stack = reason.stack;
} else {
try {
message = JSON.stringify(reason);
} catch {
message = String(reason);
}
}
reportError({
type: 'promise',
message,
stack,
timestamp: new Date().toISOString()
});
});
关键点:
event.reason可能是任何类型(Error对象、字符串、甚至undefined)- 生产环境不要使用
event.preventDefault(),否则会隐藏控制台错误 - 配合
rejectionhandled事件可以追踪后来被处理的rejection
3. 框架级错误处理
3.1 React错误边界(Error Boundary)
React 16+引入了错误边界概念,允许组件树部分崩溃而不影响整个应用:
jsx复制class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
reportError({
type: 'react',
error: error.toString(),
componentStack: info.componentStack,
location: window.location.href
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// 使用方式
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
限制:
- 无法捕获:
- 事件处理器错误
- 异步代码(setTimeout, Promise等)
- 服务端渲染错误
- 错误边界自身抛出的错误
3.2 Vue的错误处理
Vue提供了更集成的错误处理机制:
javascript复制// Vue 2
Vue.config.errorHandler = (err, vm, info) => {
reportError({
type: 'vue',
message: err.message,
component: vm?.$options.name,
lifecycleHook: info,
stack: err.stack
});
};
// Vue 3
app.config.errorHandler = (err, instance, info) => {
// 可以使用全局状态管理错误
errorStore.setError(err);
};
Vue的错误处理可以捕获:
- 组件渲染函数错误
- 观察者回调错误
- 事件处理器错误
- 生命周期钩子错误
4. 网络请求错误处理实战
4.1 Axios拦截器最佳实践
网络请求是前端主要错误来源,合理的拦截器配置可以大幅减少重复代码:
javascript复制const instance = axios.create({
timeout: 10000,
baseURL: process.env.API_BASE
});
// 请求拦截器
instance.interceptors.request.use(config => {
// 添加认证token
const token = store.getState().auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 显示loading指示器
if (config.showLoading) {
showLoading();
}
return config;
});
// 响应拦截器
instance.interceptors.response.use(
response => {
// 隐藏loading
if (response.config.showLoading) {
hideLoading();
}
// 处理业务错误码
if (response.data.code !== 0) {
return Promise.reject(createBusinessError(response.data));
}
return response.data.data;
},
error => {
// 统一错误处理
if (error.config?.showLoading) {
hideLoading();
}
const handledError = handleNetworkError(error);
// 401跳转登录
if (handledError.status === 401) {
redirectToLogin();
}
return Promise.reject(handledError);
}
);
function handleNetworkError(error) {
if (error.response) {
// 服务器响应错误(4xx, 5xx)
switch (error.response.status) {
case 400:
return createError('请求参数错误', error);
case 401:
return createError('认证失败', error);
case 403:
return createError('没有权限', error);
case 404:
return createError('资源不存在', error);
case 500:
return createError('服务器错误', error);
default:
return createError('网络错误', error);
}
} else if (error.request) {
// 请求已发出但无响应
return createError('网络连接失败', error);
} else {
// 请求配置错误
return createError('请求配置错误', error);
}
}
4.2 重试机制实现
对于网络抖动等临时性错误,自动重试可以显著提升用户体验:
javascript复制async function requestWithRetry(config, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await instance(config);
} catch (error) {
lastError = error;
// 只对网络错误和5xx错误重试
const shouldRetry = !error.response || error.response.status >= 500;
if (!shouldRetry) break;
// 指数退避算法
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
throw lastError;
}
5. 用户界面降级策略
5.1 组件级降级
当组件无法正常渲染时,提供有意义的备用UI:
jsx复制function ImageWithFallback({ src, alt, fallbackSrc }) {
const [error, setError] = useState(false);
return error ? (
<div className="image-fallback">
<img src={fallbackSrc} alt={`${alt} (备用)`} />
<p>图片加载失败</p>
</div>
) : (
<img
src={src}
alt={alt}
onError={() => setError(true)}
/>
);
}
5.2 路由级降级
对于关键路由,准备备用页面:
javascript复制// React Router示例
<Route
path="/dashboard"
element={
<ErrorBoundary fallback={<DashboardErrorPage />}>
<Dashboard />
</ErrorBoundary>
}
/>
5.3 用户友好的错误提示
避免技术术语,提供明确的操作指引:
jsx复制function ErrorMessage({ type, onRetry }) {
const errorConfigs = {
network: {
title: "网络连接问题",
description: "请检查您的网络设置后重试",
icon: "🌐"
},
payment: {
title: "支付处理失败",
description: "请尝试其他支付方式或联系客服",
icon: "💳"
},
default: {
title: "出了点问题",
description: "我们的工程师已收到通知",
icon: "⚠️"
}
};
const config = errorConfigs[type] || errorConfigs.default;
return (
<div className="error-message">
<div className="error-icon">{config.icon}</div>
<h3>{config.title}</h3>
<p>{config.description}</p>
{onRetry && (
<button onClick={onRetry}>重试</button>
)}
<a href="/support">联系客服</a>
</div>
);
}
6. 错误监控与诊断
6.1 错误上下文收集
有效的错误报告需要包含复现问题的完整上下文:
javascript复制function reportError(error, context = {}) {
const errorInfo = {
// 错误基本信息
name: error.name,
message: error.message,
stack: error.stack,
// 环境信息
url: window.location.href,
userAgent: navigator.userAgent,
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
// 性能指标
timing: {
loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
readyTime: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart
},
// 业务上下文
userId: store.getState().user.id,
version: process.env.APP_VERSION,
// 自定义上下文
...context
};
// 发送到监控系统
sendToMonitoringSystem(errorInfo);
// 本地存储用于离线情况
saveErrorLocally(errorInfo);
}
6.2 Source Map配置
生产环境使用隐藏的source map,确保错误能映射到源代码:
javascript复制// webpack.config.js
module.exports = {
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map'
: 'eval-source-map',
plugins: [
new SentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: "your-org",
project: "your-project",
release: process.env.GIT_COMMIT_SHA,
include: "./dist"
})
]
};
7. 性能与错误处理的平衡
错误处理不可避免地会带来性能开销,需要在关键路径上特别小心:
7.1 避免热路径中的try-catch
对于高频执行的代码(如滚动事件、动画帧),避免使用try-catch:
javascript复制// 不推荐
function handleScroll() {
try {
// 高频执行的代码
} catch (e) {
// ...
}
}
// 推荐:将错误处理移到外层
function safeScrollHandler() {
// 主逻辑
}
function handleScroll() {
if (shouldHandleScroll) {
safeScrollHandler();
}
}
7.2 错误采样率
对于高频错误,可以设置采样率避免过多上报:
javascript复制function reportError(error) {
// 对高频错误进行采样(10%)
if (error.name === 'ResizeObserverError' && Math.random() > 0.1) {
return;
}
// 正常上报
}
8. 测试策略
8.1 错误场景测试用例
确保错误处理代码被充分测试:
javascript复制describe('Error Handling', () => {
it('should handle API 500 error', async () => {
mockServer.mockResponse('/api/data', { status: 500 });
render(<MyComponent />);
await waitFor(() => {
expect(screen.getByText('服务器错误')).toBeInTheDocument();
});
});
it('should recover from component error', () => {
const Component = () => {
throw new Error('test');
};
render(
<ErrorBoundary>
<Component />
</ErrorBoundary>
);
expect(screen.getByText('Try Again')).toBeInTheDocument();
});
});
8.2 Chaos Engineering
在前端实施混沌工程,主动注入错误:
javascript复制// 开发环境错误注入
if (process.env.NODE_ENV === 'development') {
window.injectError = (type) => {
switch (type) {
case 'api':
mockNetworkError();
break;
case 'component':
throw new Error('Injected error');
// ...
}
};
}
9. 错误处理架构模式
9.1 分层错误处理
mermaid复制graph TD
A[用户界面层] -->|捕获| B(组件错误)
A -->|降级| C[备用UI]
D[业务逻辑层] -->|处理| E[业务异常]
D -->|转换| F[用户友好错误]
G[网络层] -->|拦截| H[HTTP错误]
G -->|重试| I[网络恢复]
J[全局] -->|兜底| K[未捕获异常]
9.2 错误分类处理
javascript复制class ErrorHandler {
handle(error) {
switch (error.type) {
case 'NETWORK':
return this.handleNetworkError(error);
case 'BUSINESS':
return this.handleBusinessError(error);
case 'FATAL':
return this.handleFatalError(error);
default:
return this.handleUnknownError(error);
}
}
handleNetworkError(error) {
// 重试逻辑
}
// ...其他处理方法
}
10. 文化与流程
10.1 错误处理检查清单
在代码审查中加入错误处理检查点:
- [ ] 关键操作是否有try-catch?
- [ ] 异步代码是否有错误处理?
- [ ] 错误消息是否用户友好?
- [ ] 是否有适当的降级方案?
- [ ] 错误是否被正确上报?
10.2 错误复盘流程
建立错误复盘机制:
- 错误发现(监控系统/用户反馈)
- 严重性评估(影响范围/发生频率)
- 根本原因分析
- 解决方案设计
- 预防措施实施
11. 进阶技巧
11.1 错误边界组合
嵌套使用错误边界实现细粒度控制:
jsx复制<ErrorBoundary fallback={<AppError />}>
<Layout>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
</Layout>
</ErrorBoundary>
11.2 错误恢复策略
javascript复制function useErrorRecovery() {
const [error, setError] = useState(null);
const retry = useCallback(() => {
setError(null);
// 重新初始化状态
}, []);
const dispatch = useCallback((action) => {
try {
// 执行业务逻辑
} catch (e) {
setError(e);
}
}, []);
return { error, retry, dispatch };
}
12. 未来趋势
12.1 前端可观测性
将错误处理纳入更广泛的可观测性体系:
- 错误监控
- 性能追踪
- 用户行为分析
- 业务指标监控
12.2 基于AI的错误诊断
利用机器学习技术:
- 自动错误分类
- 根本原因分析
- 修复建议生成
- 异常模式检测
13. 个人经验总结
在多年的前端开发中,我总结了以下错误处理原则:
- 防御性编程:假定任何外部依赖都可能失败
- 快速失败:在无法恢复的情况下尽早抛出错误
- 优雅降级:确保核心功能在部分失败时仍可用
- 透明反馈:让用户清楚知道发生了什么
- 详尽记录:保留足够的调试信息
- 持续改进:从每个错误中学习
一个健壮的前端错误处理系统需要多层次防御:
- 代码层面的try-catch
- 组件级的错误边界
- 应用级的全局捕获
- 网络请求的拦截处理
- 用户界面的优雅降级
记住:错误处理不是事后的补救措施,而是系统设计的重要组成部分。投资于健壮的错误处理,最终会为你节省无数个深夜调试的时间。