JavaScript 错误处理
大约 10 分钟约 2868 字
JavaScript 错误处理
简介
JavaScript 错误处理涵盖 try/catch、Promise 错误捕获、全局错误监听和错误上报。完善的错误处理策略有助于快速定位线上问题,提升用户体验。从前端角度看,错误分为同步错误(语法错误、类型错误、引用错误)、异步错误(Promise rejection、async/await 异常)、资源加载错误和运行时错误。建立分层错误处理体系(业务层 → 框架层 → 全局兜底),结合 Sentry 等错误监控平台,是实现生产级应用稳定性的基础。
特点
实现
自定义错误类型体系
// ========== 基础错误类 ==========
class AppError extends Error {
constructor(code, message, { cause, context } = {}) {
super(message, { cause });
this.name = 'AppError';
this.code = code;
this.context = context;
this.timestamp = new Date().toISOString();
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
context: this.context,
timestamp: this.timestamp,
cause: this.cause?.message,
};
}
}
// ========== 业务错误类型 ==========
class NetworkError extends AppError {
constructor(url, status, { cause } = {}) {
super('NETWORK_ERROR', `请求失败: ${url} (${status})`, { cause });
this.name = 'NetworkError';
this.url = url;
this.status = status;
}
}
class ValidationError extends AppError {
constructor(fields, { cause } = {}) {
const message = Object.entries(fields)
.map(([k, v]) => `${k}: ${v}`)
.join('; ');
super('VALIDATION_ERROR', message, { cause });
this.name = 'ValidationError';
this.fields = fields;
}
}
class AuthError extends AppError {
constructor(message = '认证失败', { cause } = {}) {
super('AUTH_ERROR', message, { cause });
this.name = 'AuthError';
}
}
class BusinessError extends AppError {
constructor(code, message, { cause } = {}) {
super(code, message, { cause });
this.name = 'BusinessError';
}
}
// ========== 错误码映射 ==========
const ERROR_MESSAGES = {
NETWORK_ERROR: '网络连接异常,请检查网络后重试',
VALIDATION_ERROR: '提交的数据格式不正确',
AUTH_ERROR: '登录已过期,请重新登录',
PERMISSION_ERROR: '您没有执行此操作的权限',
NOT_FOUND: '请求的资源不存在',
SERVER_ERROR: '服务器内部错误,请稍后重试',
TIMEOUT: '请求超时,请稍后重试',
};
function getUserMessage(error) {
if (error instanceof AppError) {
return ERROR_MESSAGES[error.code] || error.message;
}
return '系统异常,请稍后重试';
}
// ========== 使用 ==========
throw new NetworkError('/api/users', 500);
throw new ValidationError({ name: '不能为空', email: '格式不正确' });
throw new AuthError('Token 已过期');
throw new BusinessError('INSUFFICIENT_BALANCE', '余额不足');同步错误捕获
// ========== try/catch 基础 ==========
try {
const data = JSON.parse(invalidJson);
} catch (error) {
if (error instanceof SyntaxError) {
console.error('JSON 解析失败:', error.message);
throw new AppError('PARSE_ERROR', '数据格式错误', { cause: error });
}
throw error; // 重新抛出未知错误
}
// ========== try/catch/finally ==========
let connection;
try {
connection = await createConnection();
const result = await connection.query('SELECT * FROM users');
return result;
} catch (error) {
if (error instanceof NetworkError) {
showRetryToast(error);
} else {
showErrorNotification(getUserMessage(error));
}
reportError(error);
} finally {
// 无论成功失败都执行清理
if (connection) connection.close();
}
// ========== 错误链 ==========
try {
fetchUser(id);
} catch (error) {
throw new AppError(
'FETCH_USER_FAILED',
`获取用户 ${id} 失败`,
{ cause: error } // 保留原始错误
);
}异步错误处理
// ========== Promise 链式错误处理 ==========
fetch('/api/data')
.then(res => {
if (!res.ok) {
throw new NetworkError(res.url, res.status);
}
return res.json();
})
.then(data => processData(data))
.catch(error => {
if (error instanceof NetworkError) {
showRetryToast(error);
} else if (error instanceof ValidationError) {
showFieldErrors(error.fields);
} else {
showErrorNotification(getUserMessage(error));
}
reportError(error);
throw error; // 继续向上传播
})
.finally(() => {
hideLoadingSpinner();
});
// ========== async/await 统一错误处理 ==========
async function safeRequest(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const res = await fetch(url, {
...options,
signal: controller.signal,
});
if (res.status === 401) {
throw new AuthError('Token 已过期');
}
if (res.status === 403) {
throw new BusinessError('PERMISSION_ERROR', '无权限访问');
}
if (res.status === 404) {
throw new BusinessError('NOT_FOUND', '资源不存在');
}
if (!res.ok) {
throw new NetworkError(url, res.status);
}
clearTimeout(timeoutId);
return await res.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new AppError('TIMEOUT', '请求超时', { cause: error });
}
throw error;
}
}
// ========== 统一错误处理包装器 ==========
// 将 async 函数的 catch 转为 [error, data] 元组
async function tryCatch(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
// 使用
const [error, users] = await tryCatch(safeRequest('/api/users'));
if (error) {
showErrorNotification(getUserMessage(error));
return;
}
console.log(users);
// ========== Express/Koa 中间件错误处理 ==========
// Express
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// 使用
app.get('/users', asyncHandler(async (req, res) => {
const users = await safeRequest('/api/users');
res.json(users);
}));
// 全局错误中间件
app.use((err, req, res, next) => {
if (err instanceof AppError) {
res.status(err.status || 500).json({
code: err.code,
message: getUserMessage(err),
});
} else {
res.status(500).json({ code: 'INTERNAL_ERROR', message: '服务器错误' });
}
reportError(err);
});全局错误监控
// ========== 全局同步错误 ==========
window.onerror = function(message, source, lineno, colno, error) {
reportError({
type: 'sync_error',
message: String(message),
source,
lineno,
colno,
stack: error?.stack,
url: location.href,
});
// 返回 false — 让浏览器也显示默认错误
return false;
};
// ========== 全局 Promise 未捕获 rejection ==========
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的 Promise Rejection:', event.reason);
reportError({
type: 'unhandled_rejection',
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
url: location.href,
});
// 阻止默认控制台警告
event.preventDefault();
});
// ========== 资源加载错误 ==========
window.addEventListener('error', function(event) {
// event.target !== window 表示是资源加载错误(img/script/link)
if (event.target !== window) {
const tag = event.target.tagName;
const src = event.target.src || event.target.href;
reportError({
type: 'resource_error',
tag,
src,
url: location.href,
});
}
}, true); // capture: true 在捕获阶段监听
// ========== Vue 全局错误处理 ==========
// app.config.errorHandler = (err, instance, info) => {
// reportError({ type: 'vue_error', message: err.message, stack: err.stack, info });
// };
// ========== React 错误边界 ==========
// class ErrorBoundary extends React.Component {
// state = { hasError: false, error: null };
// static getDerivedStateFromError(error) {
// return { hasError: true, error };
// }
// componentDidCatch(error, errorInfo) {
// reportError({ type: 'react_error', ...error, componentStack: errorInfo.componentStack });
// }
// render() {
// if (this.state.hasError) return <ErrorFallback error={this.state.error} />;
// return this.props.children;
// }
// }错误上报服务
// ========== 错误上报 ==========
class ErrorReporter {
constructor(options = {}) {
this.endpoint = options.endpoint || '/api/errors';
this.appName = options.appName || 'web-app';
this.appVersion = options.appVersion || '1.0.0';
this.sampleRate = options.sampleRate || 1; // 采样率
this.maxQueueSize = options.maxQueueSize || 20;
this.queue = [];
this.flushTimer = null;
}
report(error) {
// 采样
if (Math.random() > this.sampleRate) return;
const report = {
appName: this.appName,
appVersion: this.appVersion,
timestamp: new Date().toISOString(),
url: location.href,
userAgent: navigator.userAgent,
screenSize: `${window.innerWidth}x${window.innerHeight}`,
...error,
};
this.queue.push(report);
// 批量上报 — 避免频繁请求
if (this.queue.length >= this.maxQueueSize) {
this.flush();
} else {
this.scheduleFlush();
}
}
scheduleFlush() {
if (this.flushTimer) return;
this.flushTimer = setTimeout(() => {
this.flush();
}, 5000); // 5 秒后批量上报
}
flush() {
if (this.queue.length === 0) return;
clearTimeout(this.flushTimer);
this.flushTimer = null;
const batch = this.queue.splice(0, this.maxQueueSize);
// 使用 sendBeacon — 页面卸载时也能发送
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(batch)], { type: 'application/json' });
navigator.sendBeacon(this.endpoint, blob);
} else {
fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify(batch),
keepalive: true,
}).catch(() => {
// 上报失败 — 存入 localStorage 延迟上报
const pending = JSON.parse(localStorage.getItem('pending_errors') || '[]');
pending.push(...batch);
localStorage.setItem('pending_errors', JSON.stringify(pending.slice(-50)));
});
}
}
}
// 全局实例
const errorReporter = new ErrorReporter({
endpoint: '/api/errors',
appName: 'my-web-app',
sampleRate: 0.1, // 10% 采样
});
function reportError(error) {
errorReporter.report(error);
}
// ========== 性能监控 ==========
// 错误处理不应影响用户体验
window.addEventListener('error', (event) => {
// 静默处理非关键错误
if (event.target !== window) {
// 资源加载失败 — 降级处理
const img = event.target;
if (img.tagName === 'IMG') {
img.src = '/placeholder.png'; // 替换为占位图
}
}
}, true);优点
缺点
总结
JavaScript 错误处理分为同步(try/catch)、异步(.catch/unhandledrejection)和全局(onerror/resource error)三层。自定义 Error 类型帮助分类处理不同错误。Error.cause(ES2022)支持错误链追踪。生产环境需要错误上报服务收集和分析线上错误。建立错误码映射和用户提示文案,确保用户看到友好的错误信息。
关键知识点
- try/catch 只能捕获同步错误,异步错误需要 .catch() 或 try { await } catch。
- unhandledrejection 事件捕获未处理的 Promise rejection。
- Error.cause 属性(ES2022)支持错误链追踪原始错误。
- 跨域脚本需要设置 crossorigin 属性才能获取完整错误信息。
- sendBeacon 确保页面卸载时也能发送错误上报。
项目落地视角
- 为 API 请求封装统一的错误处理拦截器。
- 全局错误上报到 Sentry 或自建错误收集服务。
- 为不同错误类型设计用户友好的提示文案。
- 配置 Source Map 上传实现生产代码堆栈还原。
常见误区
- 只用 try/catch 捕获 async 错误忘记处理 Promise rejection。
- catch 后不 throw 导致错误被静默吞掉。
- 在全局 onerror 中返回 true 阻止了默认错误输出。
- 错误上报量过大导致后端压力。
进阶路线
- 集成 Sentry/Bugsnag 实现完整的错误监控和告警。
- 配置 Source Map 上传实现生产代码堆栈还原。
- 研究前端异常监控的最佳实践。
适用场景
- API 请求错误处理和重试。
- 全局异常监控和上报。
- 用户操作错误提示。
落地建议
- 封装统一的错误处理函数,项目内统一使用。
- 为 API 请求添加错误码映射和用户提示。
- 在 CI 中集成错误监控检查。
排错清单
- 检查 Sentry 是否正确上报错误。
- 确认 Source Map 是否正确上传。
- 检查 unhandledrejection 是否被监听。
复盘问题
- 线上最常见的错误类型是什么?是否可以预防?
- 错误上报的数据量和采样策略是否合理?
- 如何区分需要上报的错误和可忽略的噪音?
错误处理设计模式
重试机制
// ========== 自动重试工具 ==========
class RetryPolicy {
constructor(options = {}) {
this.maxAttempts = options.maxAttempts || 3;
this.delay = options.delay || 1000;
this.backoffMultiplier = options.backoffMultiplier || 2;
this.maxDelay = options.maxDelay || 30000;
this.retryableErrors = options.retryableErrors || [
'NETWORK_ERROR',
'TIMEOUT',
'SERVER_ERROR',
];
}
async execute(fn) {
let lastError;
let currentDelay = this.delay;
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
try {
return await fn(attempt);
} catch (error) {
lastError = error;
// 检查是否可重试
const errorCode = error.code || error.name;
if (!this.retryableErrors.includes(errorCode)) {
throw error; // 不可重试的错误直接抛出
}
// 最后一次尝试不等待
if (attempt === this.maxAttempts) {
break;
}
console.warn(`尝试 ${attempt}/${this.maxAttempts} 失败,${currentDelay}ms 后重试: ${error.message}`);
// 指数退避 + 抖动
const jitter = Math.random() * 0.3 * currentDelay;
await new Promise(resolve => setTimeout(resolve, currentDelay + jitter));
currentDelay = Math.min(currentDelay * this.backoffMultiplier, this.maxDelay);
}
}
throw new AppError(
'MAX_RETRIES_EXCEEDED',
`操作在 ${this.maxAttempts} 次尝试后仍然失败`,
{ cause: lastError }
);
}
}
// 使用
const retryPolicy = new RetryPolicy({
maxAttempts: 3,
delay: 500,
backoffMultiplier: 2,
});
const data = await retryPolicy.execute(async (attempt) => {
console.log(`第 ${attempt} 次请求`);
return await safeRequest('/api/data');
});断路器模式
// ========== 断路器 — 防止级联故障 ==========
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.halfOpenRequests = options.halfOpenRequests || 3;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = null;
this.listeners = [];
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = 'HALF_OPEN';
this.successCount = 0;
} else {
throw new AppError('CIRCUIT_OPEN', '断路器已打开,请求被拒绝');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.halfOpenRequests) {
this.state = 'CLOSED';
this.notify('CLOSED');
}
}
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.state === 'HALF_OPEN') {
this.state = 'OPEN';
this.notify('OPEN');
} else if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.notify('OPEN');
}
}
onStateChange(listener) {
this.listeners.push(listener);
}
notify(state) {
this.listeners.forEach(listener => listener(state));
}
get status() {
return {
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime,
};
}
}
// 使用
const circuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000,
});
circuitBreaker.onStateChange((state) => {
console.warn(`断路器状态变化: ${state}`);
if (state === 'OPEN') {
reportError({ type: 'circuit_breaker_open', service: 'api' });
}
});
try {
const data = await circuitBreaker.execute(() => safeRequest('/api/data'));
} catch (error) {
if (error.code === 'CIRCUIT_OPEN') {
showErrorNotification('服务暂时不可用,请稍后重试');
}
}React 错误边界组件
// ========== React 错误边界 — 生产级实现 ==========
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
resetKeys?: unknown[];
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class AppErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// 上报错误
reportError({
type: 'react_error_boundary',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
// 自定义错误处理
this.props.onError?.(error, errorInfo);
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
// resetKeys 变化时重置错误状态
if (this.state.hasError && prevProps.resetKeys !== this.props.resetKeys) {
this.setState({ hasError: false, error: null });
}
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="error-boundary">
<h2>页面出现了问题</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.handleReset}>重试</button>
<button onClick={() => window.location.reload()}>刷新页面</button>
</div>
);
}
return this.props.children;
}
}
// 使用
function App() {
return (
<AppErrorBoundary
onError={(error, info) => console.error('Caught:', error, info)}
resetKeys={[userId]} // userId 变化时重置
>
<Router />
</AppErrorBoundary>
);
}
// 页面级错误边界
function DashboardPage() {
return (
<div>
<Header />
<AppErrorBoundary fallback={<DashboardErrorFallback />}>
<Dashboard />
</AppErrorBoundary>
</div>
);
}Vue 错误处理
// ========== Vue 3 全局错误处理 ==========
import { App, onErrorCaptured, ref } from 'vue';
// 全局错误处理器
export function setupErrorHandler(app: App) {
app.config.errorHandler = (err, instance, info) => {
const error = err as Error;
reportError({
type: 'vue_global_error',
message: error.message,
stack: error.stack,
componentName: instance?.$options?.name || 'Unknown',
info,
});
};
app.config.warnHandler = (msg, instance, trace) => {
// 生产环境忽略警告,开发环境记录
if (import.meta.env.DEV) {
console.warn(`[Vue Warn] ${msg}`, trace);
}
};
}
// 组件级错误捕获
// composables/useErrorBoundary.ts
export function useErrorBoundary() {
const error = ref<Error | null>(null);
const hasError = ref(false);
onErrorCaptured((err) => {
error.value = err;
hasError.value = true;
reportError({ type: 'vue_component_error', message: err.message });
return false; // 阻止错误继续向上传播
});
function resetError() {
error.value = null;
hasError.value = false;
}
return { error, hasError, resetError };
}