前端错误处理与日志系统完全指南
为什么需要系统化的错误处理
在生产环境中,前端错误可能导致:
- 用户操作中断,流失客户
- 关键业务流程失败
- 难以复现和排查的问题
- 品牌形象受损
没有错误处理系统 vs 有完善的错误处理系统:
没有系统:
用户遇到错误 → 白屏/卡死 → 用户离开 → 问题无人知晓
↓
问题持续存在
有系统:
用户遇到错误 → 优雅降级 → 用户看到友好提示 → 错误被捕获上报
↓
团队收到告警 ← 错误数据分析 ← 日志系统记录
↓
快速定位修复 → 问题解决
错误类型分类
JavaScript 错误类型
// 1. 语法错误(SyntaxError)
// 在编译时就会被发现,通常不会到达运行时
eval('const x = ;'); // SyntaxError
// 2. 引用错误(ReferenceError)
console.log(undefinedVariable); // ReferenceError
// 3. 类型错误(TypeError)
null.toString(); // TypeError
undefined.map(x => x); // TypeError
// 4. 范围错误(RangeError)
new Array(-1); // RangeError
(123).toFixed(200); // RangeError
// 5. URI 错误(URIError)
decodeURIComponent('%'); // URIError
// 6. 自定义错误
class BusinessError extends Error {
constructor(code, message) {
super(message);
this.name = 'BusinessError';
this.code = code;
}
}
按来源分类
错误来源分类:
┌─────────────────────────────────────────────────────────────┐
│ 前端错误来源 │
├─────────────────┬─────────────────┬─────────────────────────┤
│ JavaScript │ 资源加载 │ 网络请求 │
├─────────────────┼─────────────────┼─────────────────────────┤
│ • 运行时错误 │ • 脚本加载失败 │ • API 请求失败 │
│ • Promise 拒绝 │ • 样式加载失败 │ • 超时 │
│ • 事件处理错误 │ • 图片加载失败 │ • 网络断开 │
│ • 异步错误 │ • 字体加载失败 │ • CORS 错误 │
└─────────────────┴─────────────────┴─────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ 框架特定错误 │
├─────────────────┬─────────────────┬─────────────────────────┤
│ Vue │ React │ 通用 │
├─────────────────┼─────────────────┼─────────────────────────┤
│ • 组件渲染错误 │ • 渲染错误 │ • 路由错误 │
│ • 生命周期错误 │ • Hooks 错误 │ • 状态管理错误 │
│ • Watcher 错误 │ • 事件处理错误 │ • 第三方库错误 │
└─────────────────┴─────────────────┴─────────────────────────┘
全局错误捕获
原生 JavaScript 错误捕获
// 1. 同步错误捕获
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global Error:', {
message,
source,
lineno,
colno,
stack: error?.stack
});
// 上报错误
reportError({
type: 'javascript',
message,
source,
lineno,
colno,
stack: error?.stack
});
// 返回 true 阻止默认错误处理
return true;
};
// 2. Promise 未捕获拒绝
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise Rejection:', event.reason);
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack
});
// 阻止默认处理
event.preventDefault();
});
// 3. 资源加载错误
window.addEventListener('error', (event) => {
// 区分 JS 错误和资源加载错误
if (event.target && (event.target.src || event.target.href)) {
const target = event.target;
reportError({
type: 'resource',
tagName: target.tagName,
src: target.src || target.href,
message: `Failed to load ${target.tagName.toLowerCase()}`
});
}
}, true); // 使用捕获阶段
Vue 3 错误处理
// plugins/errorHandler.ts
import type { App } from 'vue';
export function setupErrorHandler(app: App) {
// 全局错误处理器
app.config.errorHandler = (err, instance, info) => {
console.error('Vue Error:', err);
const error = {
type: 'vue',
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
componentName: instance?.$options?.name || 'Unknown',
info, // 如 'setup function', 'render function' 等
props: instance?.$props,
route: instance?.$route?.fullPath
};
reportError(error);
};
// 警告处理器(开发环境)
if (process.env.NODE_ENV !== 'production') {
app.config.warnHandler = (msg, instance, trace) => {
console.warn('Vue Warning:', msg);
console.warn('Component trace:', trace);
};
}
}
// main.ts
import { createApp } from 'vue';
import { setupErrorHandler } from './plugins/errorHandler';
const app = createApp(App);
setupErrorHandler(app);
app.mount('#app');
Nuxt 3 错误处理
// plugins/error-handler.client.ts
export default defineNuxtPlugin((nuxtApp) => {
// Vue 错误
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
console.error('Vue Error:', error);
// 使用 Nuxt 的错误处理
showError({
statusCode: 500,
message: error instanceof Error ? error.message : 'Unknown error'
});
// 上报错误
reportError({
type: 'vue',
error,
info,
componentName: instance?.$options?.name
});
};
// 全局错误钩子
nuxtApp.hook('vue:error', (error, instance, info) => {
console.error('Nuxt Vue Error Hook:', error);
});
// 应用错误钩子
nuxtApp.hook('app:error', (error) => {
console.error('Nuxt App Error:', error);
});
});
// error.vue - 全局错误页面
<script setup lang="ts">
const props = defineProps({
error: Object
});
const handleError = () => clearError({ redirect: '/' });
</script>
<template>
<div class="error-page">
<h1>{{ error?.statusCode || 500 }}</h1>
<p>{{ error?.message || '出错了' }}</p>
<button @click="handleError">返回首页</button>
</div>
</template>
错误边界组件
Vue 错误边界
<!-- components/ErrorBoundary.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue';
const props = defineProps<{
fallback?: string;
onError?: (error: Error, info: string) => void;
}>();
const hasError = ref(false);
const errorMessage = ref('');
onErrorCaptured((error, instance, info) => {
hasError.value = true;
errorMessage.value = error.message;
// 调用自定义错误处理
props.onError?.(error, info);
// 上报错误
reportError({
type: 'component',
message: error.message,
stack: error.stack,
info
});
// 返回 false 阻止错误继续向上传播
return false;
});
const retry = () => {
hasError.value = false;
errorMessage.value = '';
};
</script>
<template>
<div v-if="hasError" class="error-boundary">
<slot name="fallback">
<div class="error-content">
<h3>{{ fallback || '组件加载失败' }}</h3>
<p class="error-message">{{ errorMessage }}</p>
<button @click="retry">重试</button>
</div>
</slot>
</div>
<slot v-else />
</template>
<style scoped>
.error-boundary {
padding: 20px;
border: 1px solid #f5c6cb;
background: #f8d7da;
border-radius: 8px;
}
.error-content {
text-align: center;
}
.error-message {
color: #721c24;
font-size: 14px;
}
</style>
使用错误边界
<template>
<div class="app">
<!-- 关键组件使用错误边界包裹 -->
<ErrorBoundary
fallback="数据加载失败"
@error="handleComponentError"
>
<DataVisualization :data="chartData" />
</ErrorBoundary>
<!-- 带自定义 fallback -->
<ErrorBoundary>
<template #fallback>
<div class="custom-error">
<img src="/error-illustration.svg" alt="Error" />
<p>图表组件暂时无法显示</p>
<button @click="refreshData">刷新数据</button>
</div>
</template>
<ComplexChart :config="chartConfig" />
</ErrorBoundary>
</div>
</template>
<script setup>
const handleComponentError = (error, info) => {
console.error('组件错误:', error);
// 可以显示全局通知
showNotification({
type: 'error',
message: '部分内容加载失败,请刷新重试'
});
};
</script>
API 错误处理
统一的请求错误处理
// utils/request.ts
import axios, { AxiosError, AxiosResponse } from 'axios';
// 错误类型定义
interface ApiError {
code: string;
message: string;
details?: Record<string, any>;
timestamp: number;
requestId?: string;
}
// 创建 axios 实例
const request = axios.create({
baseURL: '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 添加请求 ID 用于追踪
config.headers['X-Request-ID'] = generateRequestId();
// 添加认证 token
const token = getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
reportError({
type: 'request',
phase: 'request',
message: error.message
});
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
return response.data;
},
(error: AxiosError<ApiError>) => {
const errorInfo = handleApiError(error);
// 上报错误
reportError({
type: 'api',
...errorInfo
});
return Promise.reject(errorInfo);
}
);
// API 错误处理函数
function handleApiError(error: AxiosError<ApiError>) {
// 网络错误
if (!error.response) {
if (error.code === 'ECONNABORTED') {
return {
code: 'TIMEOUT',
message: '请求超时,请检查网络连接',
retryable: true
};
}
return {
code: 'NETWORK_ERROR',
message: '网络连接失败,请检查网络设置',
retryable: true
};
}
const { status, data } = error.response;
const requestId = error.config?.headers?.['X-Request-ID'];
// HTTP 状态码处理
switch (status) {
case 400:
return {
code: 'BAD_REQUEST',
message: data?.message || '请求参数错误',
details: data?.details,
retryable: false
};
case 401:
// 触发登出或刷新 token
handleUnauthorized();
return {
code: 'UNAUTHORIZED',
message: '登录已过期,请重新登录',
retryable: false
};
case 403:
return {
code: 'FORBIDDEN',
message: '没有权限执行此操作',
retryable: false
};
case 404:
return {
code: 'NOT_FOUND',
message: '请求的资源不存在',
retryable: false
};
case 422:
return {
code: 'VALIDATION_ERROR',
message: data?.message || '数据验证失败',
details: data?.details,
retryable: false
};
case 429:
return {
code: 'RATE_LIMITED',
message: '请求过于频繁,请稍后再试',
retryAfter: error.response.headers['retry-after'],
retryable: true
};
case 500:
return {
code: 'SERVER_ERROR',
message: '服务器错误,请稍后重试',
requestId,
retryable: true
};
case 502:
case 503:
case 504:
return {
code: 'SERVICE_UNAVAILABLE',
message: '服务暂时不可用,请稍后重试',
requestId,
retryable: true
};
default:
return {
code: 'UNKNOWN_ERROR',
message: data?.message || '发生未知错误',
requestId,
retryable: false
};
}
}
// 请求重试封装
async function requestWithRetry<T>(
config: any,
options: { maxRetries?: number; retryDelay?: number } = {}
): Promise<T> {
const { maxRetries = 3, retryDelay = 1000 } = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await request(config);
} catch (error: any) {
lastError = error;
// 不可重试的错误直接抛出
if (!error.retryable || attempt === maxRetries) {
throw error;
}
// 等待后重试
await sleep(retryDelay * Math.pow(2, attempt)); // 指数退避
}
}
throw lastError;
}
export { request, requestWithRetry };
日志系统设计
日志级别和格式
// utils/logger.ts
enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
FATAL = 4
}
interface LogEntry {
level: LogLevel;
message: string;
timestamp: number;
context?: Record<string, any>;
userId?: string;
sessionId?: string;
pageUrl?: string;
userAgent?: string;
stack?: string;
}
class Logger {
private static instance: Logger;
private minLevel: LogLevel;
private buffer: LogEntry[] = [];
private bufferSize: number = 10;
private flushInterval: number = 5000;
private endpoint: string;
private constructor() {
this.minLevel = process.env.NODE_ENV === 'production'
? LogLevel.INFO
: LogLevel.DEBUG;
this.endpoint = '/api/logs';
// 定时刷新日志
setInterval(() => this.flush(), this.flushInterval);
// 页面卸载前刷新
window.addEventListener('beforeunload', () => this.flush());
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
private createEntry(
level: LogLevel,
message: string,
context?: Record<string, any>
): LogEntry {
return {
level,
message,
timestamp: Date.now(),
context,
userId: getUserId(),
sessionId: getSessionId(),
pageUrl: window.location.href,
userAgent: navigator.userAgent
};
}
private log(level: LogLevel, message: string, context?: Record<string, any>) {
if (level < this.minLevel) return;
const entry = this.createEntry(level, message, context);
// 开发环境输出到控制台
if (process.env.NODE_ENV !== 'production') {
const consoleFn = this.getConsoleFn(level);
consoleFn(`[${LogLevel[level]}]`, message, context || '');
}
// 生产环境加入缓冲区
if (process.env.NODE_ENV === 'production') {
this.buffer.push(entry);
// 缓冲区满或高优先级日志立即刷新
if (this.buffer.length >= this.bufferSize || level >= LogLevel.ERROR) {
this.flush();
}
}
}
private getConsoleFn(level: LogLevel) {
switch (level) {
case LogLevel.DEBUG: return console.debug;
case LogLevel.INFO: return console.info;
case LogLevel.WARN: return console.warn;
case LogLevel.ERROR:
case LogLevel.FATAL: return console.error;
default: return console.log;
}
}
private async flush() {
if (this.buffer.length === 0) return;
const logs = [...this.buffer];
this.buffer = [];
try {
// 使用 sendBeacon 确保页面卸载时也能发送
if (navigator.sendBeacon) {
navigator.sendBeacon(
this.endpoint,
JSON.stringify({ logs })
);
} else {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ logs }),
keepalive: true
});
}
} catch (error) {
// 发送失败,放回缓冲区
this.buffer = [...logs, ...this.buffer].slice(0, this.bufferSize * 2);
}
}
// 公开方法
debug(message: string, context?: Record<string, any>) {
this.log(LogLevel.DEBUG, message, context);
}
info(message: string, context?: Record<string, any>) {
this.log(LogLevel.INFO, message, context);
}
warn(message: string, context?: Record<string, any>) {
this.log(LogLevel.WARN, message, context);
}
error(message: string, context?: Record<string, any>) {
this.log(LogLevel.ERROR, message, context);
}
fatal(message: string, context?: Record<string, any>) {
this.log(LogLevel.FATAL, message, context);
}
// 性能日志
performance(name: string, duration: number, context?: Record<string, any>) {
this.info(`Performance: ${name}`, { ...context, duration });
}
// 用户行为日志
track(event: string, properties?: Record<string, any>) {
this.info(`Track: ${event}`, properties);
}
}
export const logger = Logger.getInstance();
结构化日志
// 业务日志示例
import { logger } from '@/utils/logger';
// 用户操作日志
function handleLogin(username: string) {
logger.info('User login attempt', {
username,
method: 'password'
});
try {
await loginApi({ username, password });
logger.info('User login success', { username });
} catch (error) {
logger.error('User login failed', {
username,
errorCode: error.code,
errorMessage: error.message
});
}
}
// 业务流程日志
async function processOrder(orderId: string) {
const traceId = generateTraceId();
logger.info('Order processing started', { orderId, traceId });
try {
// 验证库存
logger.debug('Checking inventory', { orderId, traceId });
await checkInventory(orderId);
// 处理支付
logger.debug('Processing payment', { orderId, traceId });
const paymentResult = await processPayment(orderId);
logger.info('Payment processed', {
orderId,
traceId,
transactionId: paymentResult.transactionId
});
// 创建发货单
logger.debug('Creating shipment', { orderId, traceId });
await createShipment(orderId);
logger.info('Order processing completed', { orderId, traceId });
} catch (error) {
logger.error('Order processing failed', {
orderId,
traceId,
step: error.step,
error: error.message
});
throw error;
}
}
// 性能日志
function measureApiCall() {
const start = performance.now();
try {
const result = await fetchData();
const duration = performance.now() - start;
logger.performance('API fetchData', duration, {
endpoint: '/api/data',
resultCount: result.length
});
return result;
} catch (error) {
const duration = performance.now() - start;
logger.performance('API fetchData (failed)', duration, {
endpoint: '/api/data',
error: error.message
});
throw error;
}
}
错误上报系统
错误上报服务
// services/errorReporter.ts
interface ErrorReport {
type: string;
message: string;
stack?: string;
context?: Record<string, any>;
timestamp: number;
userId?: string;
sessionId?: string;
pageUrl: string;
userAgent: string;
screenSize: string;
// 性能信息
performance?: {
loadTime?: number;
memoryUsage?: number;
};
}
class ErrorReporter {
private endpoint: string;
private queue: ErrorReport[] = [];
private sending: boolean = false;
private sampleRate: number;
constructor(options: { endpoint: string; sampleRate?: number }) {
this.endpoint = options.endpoint;
this.sampleRate = options.sampleRate || 1; // 默认全量上报
// 定时发送队列中的错误
setInterval(() => this.processQueue(), 5000);
}
report(error: Partial<ErrorReport>) {
// 采样
if (Math.random() > this.sampleRate) return;
const report: ErrorReport = {
type: error.type || 'unknown',
message: error.message || 'Unknown error',
stack: error.stack,
context: error.context,
timestamp: Date.now(),
userId: getUserId(),
sessionId: getSessionId(),
pageUrl: window.location.href,
userAgent: navigator.userAgent,
screenSize: `${window.screen.width}x${window.screen.height}`,
performance: this.getPerformanceInfo()
};
// 去重处理
if (this.isDuplicate(report)) return;
this.queue.push(report);
// 严重错误立即发送
if (error.type === 'fatal' || error.type === 'unhandledrejection') {
this.processQueue();
}
}
private isDuplicate(report: ErrorReport): boolean {
// 简单去重:相同错误在短时间内只上报一次
const key = `${report.type}-${report.message}`;
const lastReport = this.lastReports.get(key);
if (lastReport && Date.now() - lastReport < 60000) {
return true;
}
this.lastReports.set(key, Date.now());
return false;
}
private lastReports = new Map<string, number>();
private getPerformanceInfo() {
if (typeof window === 'undefined') return undefined;
const timing = performance.timing;
const memory = (performance as any).memory;
return {
loadTime: timing.loadEventEnd - timing.navigationStart,
memoryUsage: memory?.usedJSHeapSize
};
}
private async processQueue() {
if (this.sending || this.queue.length === 0) return;
this.sending = true;
const batch = this.queue.splice(0, 10); // 每次最多发送 10 条
try {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ errors: batch }),
keepalive: true
});
} catch (e) {
// 发送失败,放回队列
this.queue.unshift(...batch);
} finally {
this.sending = false;
}
}
}
// 全局错误上报实例
export const errorReporter = new ErrorReporter({
endpoint: '/api/errors',
sampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1 // 生产环境 10% 采样
});
// 便捷方法
export function reportError(error: Partial<ErrorReport>) {
errorReporter.report(error);
}
第三方错误监控集成
// 集成 Sentry
import * as Sentry from '@sentry/vue';
export function initSentry(app: App, router: Router) {
Sentry.init({
app,
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
}),
new Sentry.Replay({
// 会话回放配置
maskAllText: true,
blockAllMedia: true
})
],
// 采样率
tracesSampleRate: 0.1, // 10% 的事务
replaysSessionSampleRate: 0.1, // 10% 的会话回放
replaysOnErrorSampleRate: 1.0, // 错误时 100% 回放
// 过滤敏感信息
beforeSend(event) {
// 过滤敏感数据
if (event.request?.data) {
delete event.request.data.password;
delete event.request.data.token;
}
return event;
},
// 忽略特定错误
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured'
]
});
}
// 手动上报
export function captureException(error: Error, context?: Record<string, any>) {
Sentry.captureException(error, {
extra: context,
tags: {
feature: context?.feature
}
});
}
用户友好的错误提示
错误消息转换
// utils/errorMessages.ts
const errorMessages: Record<string, string> = {
// 网络错误
NETWORK_ERROR: '网络连接失败,请检查网络设置',
TIMEOUT: '请求超时,请稍后重试',
// 认证错误
UNAUTHORIZED: '登录已过期,请重新登录',
FORBIDDEN: '没有权限执行此操作',
// 业务错误
INSUFFICIENT_BALANCE: '余额不足,请先充值',
STOCK_EMPTY: '商品已售罄',
ORDER_EXPIRED: '订单已过期',
// 通用错误
SERVER_ERROR: '服务器繁忙,请稍后重试',
UNKNOWN_ERROR: '操作失败,请重试'
};
export function getErrorMessage(error: any): string {
// 已经是友好消息
if (typeof error === 'string') return error;
// 有预定义的消息
if (error.code && errorMessages[error.code]) {
return errorMessages[error.code];
}
// 有消息字段
if (error.message) {
// 避免展示技术性的错误消息
if (error.message.includes('undefined') ||
error.message.includes('null') ||
error.message.includes('TypeError')) {
return '操作失败,请重试';
}
return error.message;
}
return '操作失败,请重试';
}
// 带操作建议的错误处理
export function handleError(error: any): { message: string; action?: () => void; actionText?: string } {
const code = error.code || 'UNKNOWN_ERROR';
switch (code) {
case 'UNAUTHORIZED':
return {
message: '登录已过期',
action: () => router.push('/login'),
actionText: '重新登录'
};
case 'NETWORK_ERROR':
return {
message: '网络连接失败',
action: () => window.location.reload(),
actionText: '刷新页面'
};
case 'INSUFFICIENT_BALANCE':
return {
message: '余额不足',
action: () => router.push('/wallet/recharge'),
actionText: '去充值'
};
default:
return {
message: getErrorMessage(error)
};
}
}
最佳实践总结
错误处理与日志系统最佳实践:
错误捕获:
✓ 全局捕获 window.onerror 和 unhandledrejection
✓ 使用错误边界隔离组件错误
✓ API 请求统一错误处理
✓ 异步操作使用 try-catch
错误上报:
✓ 收集足够的上下文信息
✓ 错误去重和采样
✓ 使用批量发送减少请求
✓ 敏感信息过滤
日志系统:
✓ 定义清晰的日志级别
✓ 结构化日志便于分析
✓ 包含追踪 ID 关联请求
✓ 开发和生产环境区分处理
用户体验:
✓ 提供友好的错误消息
✓ 给出可操作的建议
✓ 错误状态可恢复
✓ 避免白屏和卡死
监控告警:
✓ 设置错误率告警阈值
✓ 关键业务流程重点监控
✓ 定期分析错误趋势
✓ 快速响应严重错误
完善的错误处理和日志系统是高质量前端应用的基础设施。它不仅能帮助快速定位和修复问题,还能持续改进用户体验和系统稳定性。


