优雅的 async/await 错误处理模式指南
告别手动 try-catch,拥抱更优雅的异步错误处理方案
🎯 背景
在现代前端开发中,async/await
已经成为处理异步操作的主流方式。然而,传统的错误处理需要大量的 try-catch
块,代码冗余且不够优雅。本文将介绍几种更优雅的错误处理模式,让你的代码更加简洁和可维护。
😕 传统的 try-catch 问题
常见的冗余代码
javascript
// 传统方式 - 每个 async 函数都需要 try-catch
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
} catch (error) {
console.error('获取用户数据失败:', error);
throw error;
}
}
async function updateUserProfile(userId, profileData) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(profileData),
});
return await response.json();
} catch (error) {
console.error('更新用户资料失败:', error);
throw error;
}
}
// 使用时还需要再次 try-catch
async function handleUserUpdate() {
try {
const user = await fetchUserData(123);
const result = await updateUserProfile(123, { name: '新名字' });
console.log('更新成功', result);
} catch (error) {
// 错误处理
showErrorMessage(error.message);
}
}
问题分析
- 代码重复:每个异步函数都需要类似的错误处理
- 嵌套复杂:多层 try-catch 导致代码难以阅读
- 维护困难:错误处理逻辑分散,难以统一管理
- 忘记处理:容易遗漏某些异步操作的错误处理
🚀 优雅的错误处理方案
1. to() 函数模式 - Go 语言风格
这是一种非常流行的错误处理模式,灵感来自 Go 语言的错误处理方式。
javascript
/**
* 将 Promise 转换为 [error, data] 元组
* @param {Promise} promise - 要处理的 Promise
* @returns {Promise<[Error|null, any]>} 返回 [错误, 数据] 的元组
*/
function to(promise) {
return promise.then((data) => [null, data]).catch((error) => [error, null]);
}
// 使用示例
async function fetchUserData(userId) {
const [error, data] = await to(
fetch(`/api/users/${userId}`).then((res) => res.json())
);
if (error) {
console.error('获取用户数据失败:', error);
return null;
}
return data;
}
async function handleUserOperation() {
const [userError, user] = await to(fetchUserData(123));
if (userError) {
showErrorMessage('无法获取用户信息');
return;
}
const [updateError, result] = await to(
updateUserProfile(123, { name: '新名字' })
);
if (updateError) {
showErrorMessage('更新失败,请重试');
return;
}
console.log('操作成功', result);
}
2. 增强版 to() 函数
支持类型安全和更多功能的版本:
typescript
/**
* 增强版错误处理函数
* @param promise - 要处理的 Promise
* @param errorExt - 可选的错误扩展对象
*/
function to<T, U = Error>(
promise: Promise<T>,
errorExt?: object
): Promise<[U, undefined] | [null, T]> {
return promise
.then<[null, T]>((data: T) => [null, data])
.catch<[U, undefined]>((err: U) => {
if (errorExt) {
const parsedError = Object.assign(err, errorExt);
return [parsedError, undefined];
}
return [err, undefined];
});
}
// TypeScript 使用示例
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<User | null> {
const [error, user] = await to<User>(
fetch(`/api/users/${id}`).then((res) => res.json()),
{ operation: 'fetchUser', userId: id }
);
if (error) {
console.error('获取用户失败:', error);
return null;
}
return user;
}
3. 错误处理装饰器模式
使用装饰器来自动处理错误:
javascript
/**
* 错误处理装饰器
* @param {Function} errorHandler - 错误处理函数
*/
function withErrorHandling(errorHandler) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
return errorHandler(error, propertyKey, args);
}
};
return descriptor;
};
}
// 使用示例
class UserService {
@withErrorHandling((error, method, args) => {
console.error(`${method} 方法执行失败:`, error);
return null;
})
async fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
@withErrorHandling((error, method, args) => {
console.error(`${method} 方法执行失败:`, error);
throw new Error('用户操作失败,请重试');
})
async updateUser(id, data) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return response.json();
}
}
4. 链式错误处理模式
创建一个支持链式调用的错误处理类:
javascript
class AsyncChain {
constructor(promise = Promise.resolve()) {
this.promise = promise;
this.errors = [];
}
static of(promise) {
return new AsyncChain(promise);
}
then(fn) {
this.promise = this.promise.then(async (data) => {
if (this.errors.length > 0) return data;
try {
return await fn(data);
} catch (error) {
this.errors.push(error);
return data;
}
});
return this;
}
catch(errorHandler) {
this.promise = this.promise.then((data) => {
if (this.errors.length > 0) {
errorHandler(this.errors);
this.errors = [];
}
return data;
});
return this;
}
finally(finallyHandler) {
this.promise = this.promise.finally(finallyHandler);
return this;
}
execute() {
return this.promise;
}
}
// 使用示例
async function handleComplexOperation() {
const result = await AsyncChain.of(Promise.resolve({ userId: 123 }))
.then(async (data) => {
const user = await fetch(`/api/users/${data.userId}`).then((r) =>
r.json()
);
return { ...data, user };
})
.then(async (data) => {
const profile = await fetch(`/api/profiles/${data.user.id}`).then((r) =>
r.json()
);
return { ...data, profile };
})
.then(async (data) => {
await fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({ event: 'user_viewed', userId: data.user.id }),
});
return data;
})
.catch((errors) => {
console.error('操作中出现错误:', errors);
showErrorMessage('操作失败,请重试');
})
.execute();
return result;
}
5. 结果包装器模式
创建一个统一的结果包装器:
javascript
class Result {
constructor(success, data, error) {
this.success = success;
this.data = data;
this.error = error;
}
static ok(data) {
return new Result(true, data, null);
}
static error(error) {
return new Result(false, null, error);
}
static async from(promise) {
try {
const data = await promise;
return Result.ok(data);
} catch (error) {
return Result.error(error);
}
}
isOk() {
return this.success;
}
isError() {
return !this.success;
}
unwrap() {
if (this.success) {
return this.data;
}
throw this.error;
}
unwrapOr(defaultValue) {
return this.success ? this.data : defaultValue;
}
map(fn) {
if (this.success) {
try {
return Result.ok(fn(this.data));
} catch (error) {
return Result.error(error);
}
}
return this;
}
mapError(fn) {
if (this.isError()) {
return Result.error(fn(this.error));
}
return this;
}
}
// 使用示例
async function fetchUserProfile(userId) {
const userResult = await Result.from(
fetch(`/api/users/${userId}`).then((r) => r.json())
);
if (userResult.isError()) {
return userResult.mapError(
(error) => new Error(`获取用户失败: ${error.message}`)
);
}
const profileResult = await Result.from(
fetch(`/api/profiles/${userId}`).then((r) => r.json())
);
return profileResult.map((profile) => ({
user: userResult.data,
profile,
}));
}
// 链式操作
async function handleUserData(userId) {
const result = await fetchUserProfile(userId);
const finalData = result
.map((data) => ({
...data,
displayName: data.user.name || data.profile.nickname,
}))
.map((data) => ({
...data,
isVip: data.profile.level > 5,
}))
.unwrapOr({ displayName: '未知用户', isVip: false });
console.log('处理结果:', finalData);
}
🛠️ 实用工具函数库
创建一个完整的错误处理工具库:
javascript
// errorUtils.js
export class ErrorUtils {
/**
* Go 风格的错误处理
*/
static to(promise) {
return promise.then((data) => [null, data]).catch((error) => [error, null]);
}
/**
* 带重试的异步操作
*/
static async retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const [error, result] = await ErrorUtils.to(fn());
if (!error) {
return [null, result];
}
if (attempt === maxAttempts) {
return [error, null];
}
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
}
}
/**
* 超时处理
*/
static withTimeout(promise, timeout = 5000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('操作超时')), timeout);
});
return Promise.race([promise, timeoutPromise]);
}
/**
* 条件执行
*/
static async when(condition, asyncFn) {
if (typeof condition === 'function') {
condition = await condition();
}
if (condition) {
return await asyncFn();
}
return null;
}
/**
* 安全的 JSON 解析
*/
static safeJsonParse(str, defaultValue = null) {
try {
return JSON.parse(str);
} catch {
return defaultValue;
}
}
/**
* 批量处理异步操作
*/
static async parallel(
operations,
{ maxConcurrency = 5, failFast = false } = {}
) {
const results = [];
const errors = [];
for (let i = 0; i < operations.length; i += maxConcurrency) {
const batch = operations.slice(i, i + maxConcurrency);
const batchPromises = batch.map(async (op, index) => {
const [error, result] = await ErrorUtils.to(op());
if (error) {
errors.push({ index: i + index, error });
if (failFast) {
throw error;
}
return null;
}
return result;
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return { results, errors };
}
}
// 使用示例
async function complexDataProcessing() {
// 1. 带重试的数据获取
const [fetchError, userData] = await ErrorUtils.retry(
() => fetch('/api/user').then((r) => r.json()),
3,
1000
);
if (fetchError) {
console.error('获取用户数据失败:', fetchError);
return;
}
// 2. 并行处理多个操作
const operations = userData.items.map((item) => () => processItem(item.id));
const { results, errors } = await ErrorUtils.parallel(operations, {
maxConcurrency: 3,
failFast: false,
});
if (errors.length > 0) {
console.warn('部分操作失败:', errors);
}
console.log('处理完成:', results.filter(Boolean));
}
🎨 在 Vue/React 中的应用
Vue 3 Composition API
javascript
// composables/useAsyncData.js
import { ref, unref } from 'vue';
import { ErrorUtils } from './errorUtils';
export function useAsyncData(asyncFn, options = {}) {
const loading = ref(false);
const data = ref(null);
const error = ref(null);
const {
immediate = true,
resetOnExecute = true,
onError,
onSuccess,
} = options;
const execute = async (...args) => {
if (resetOnExecute) {
data.value = null;
error.value = null;
}
loading.value = true;
const [err, result] = await ErrorUtils.to(asyncFn(...args.map(unref)));
loading.value = false;
if (err) {
error.value = err;
onError?.(err);
} else {
data.value = result;
onSuccess?.(result);
}
return [err, result];
};
if (immediate) {
execute();
}
return {
loading: readonly(loading),
data: readonly(data),
error: readonly(error),
execute,
};
}
// 在组件中使用
export default {
setup() {
const { loading, data, error, execute } = useAsyncData(
(userId) => fetch(`/api/users/${userId}`).then((r) => r.json()),
{
immediate: false,
onError: (err) => console.error('请求失败:', err),
onSuccess: (data) => console.log('请求成功:', data),
}
);
const loadUser = (id) => execute(id);
return {
loading,
data,
error,
loadUser,
};
},
};
React Hook
javascript
// hooks/useAsyncOperation.js
import { useState, useCallback } from 'react';
import { ErrorUtils } from '../utils/errorUtils';
export function useAsyncOperation(asyncFn, options = {}) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const { onError, onSuccess, resetOnExecute = true } = options;
const execute = useCallback(
async (...args) => {
if (resetOnExecute) {
setData(null);
setError(null);
}
setLoading(true);
const [err, result] = await ErrorUtils.to(asyncFn(...args));
setLoading(false);
if (err) {
setError(err);
onError?.(err);
} else {
setData(result);
onSuccess?.(result);
}
return [err, result];
},
[asyncFn, onError, onSuccess, resetOnExecute]
);
return {
loading,
data,
error,
execute,
};
}
// 在组件中使用
function UserProfile({ userId }) {
const { loading, data, error, execute } = useAsyncOperation(
(id) => fetch(`/api/users/${id}`).then((r) => r.json()),
{
onError: (err) => toast.error('加载用户信息失败'),
onSuccess: (data) => console.log('用户信息加载成功', data),
}
);
useEffect(() => {
if (userId) {
execute(userId);
}
}, [userId, execute]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return null;
return <div>{/* 渲染用户信息 */}</div>;
}
📈 最佳实践
1. 选择合适的模式
- 简单场景 :使用
to()
函数 - 复杂逻辑:使用 Result 包装器
- 重复操作:使用装饰器模式
- 链式操作:使用 AsyncChain
2. 错误分类处理
javascript
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.name = 'AppError';
}
}
class ValidationError extends AppError {
constructor(message, field) {
super(message, 'VALIDATION_ERROR', 400);
this.field = field;
}
}
class NetworkError extends AppError {
constructor(message) {
super(message, 'NETWORK_ERROR', 0);
}
}
// 错误处理中心
class ErrorHandler {
static handle(error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof NetworkError) {
showNetworkError();
} else if (error instanceof AppError) {
showError(error.message);
} else {
console.error('未知错误:', error);
showError('系统错误,请稍后重试');
}
}
}
3. 统一的 API 错误处理
javascript
// api.js
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const [error, response] = await ErrorUtils.to(
ErrorUtils.withTimeout(
fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
}),
10000
)
);
if (error) {
throw new NetworkError('网络请求失败');
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
switch (response.status) {
case 400:
throw new ValidationError(errorData.message || '请求参数错误');
case 401:
throw new AppError('未授权访问', 'UNAUTHORIZED', 401);
case 403:
throw new AppError('权限不足', 'FORBIDDEN', 403);
case 404:
throw new AppError('资源不存在', 'NOT_FOUND', 404);
default:
throw new AppError('服务器错误', 'SERVER_ERROR', response.status);
}
}
return response.json();
}
get(endpoint, options) {
return this.request(endpoint, { method: 'GET', ...options });
}
post(endpoint, data, options) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
...options,
});
}
}
const api = new ApiClient('/api');
// 使用示例
async function createUser(userData) {
const [error, user] = await ErrorUtils.to(api.post('/users', userData));
if (error) {
ErrorHandler.handle(error);
return null;
}
return user;
}
🔧 工具推荐
NPM 包
- await-to-js - 最受欢迎的 to() 函数实现
- neverthrow - Result 类型的完整实现
- p-retry - 专业的重试工具
- p-timeout - Promise 超时处理
IDE 插件
- Error Lens - VS Code 错误高亮显示
- ESLint - 配置 async/await 相关规则
📚 总结
优雅的 async/await 错误处理不仅能让代码更简洁,还能提高代码的可维护性和可读性。选择合适的模式,结合项目的实际需求,可以大大提升开发效率。
推荐方案选择
- 新项目:优先使用 Result 包装器模式
- 现有项目改造:使用 to() 函数渐进式改进
- 团队协作:统一使用工具函数库
- 复杂应用:结合多种模式,分场景使用
记住,最好的错误处理方案是团队都能理解和维护的方案。选择适合你项目的模式,保持一致性,让代码更加优雅!
🌟 更多实际应用场景
1. 电商购物车场景
javascript
// 购物车操作的复杂错误处理
class ShoppingCartService {
constructor(apiClient) {
this.api = apiClient;
}
// 添加商品到购物车
async addToCart(productId, quantity = 1) {
// 1. 验证商品库存
const [stockError, stockInfo] = await ErrorUtils.to(
this.api.get(`/products/${productId}/stock`)
);
if (stockError) {
throw new AppError('无法获取商品库存信息', 'STOCK_CHECK_FAILED');
}
if (stockInfo.available < quantity) {
throw new ValidationError('库存不足', 'quantity');
}
// 2. 检查用户购物车容量
const [cartError, currentCart] = await ErrorUtils.to(this.api.get('/cart'));
if (cartError) {
throw new AppError('无法获取购物车信息', 'CART_FETCH_FAILED');
}
if (currentCart.items.length >= 50) {
throw new ValidationError('购物车商品数量已达上限', 'cart_limit');
}
// 3. 添加商品
const [addError, result] = await ErrorUtils.to(
this.api.post('/cart/items', {
productId,
quantity,
})
);
if (addError) {
if (addError.code === 'PRODUCT_UNAVAILABLE') {
throw new AppError('商品已下架', 'PRODUCT_UNAVAILABLE');
}
throw new AppError('添加失败,请重试', 'ADD_TO_CART_FAILED');
}
return result;
}
// 批量更新购物车
async batchUpdateCart(updates) {
const results = [];
const failures = [];
for (const update of updates) {
const [error, result] = await ErrorUtils.to(
this.updateCartItem(update.itemId, update.quantity)
);
if (error) {
failures.push({
itemId: update.itemId,
error: error.message,
});
} else {
results.push(result);
}
}
return {
success: results,
failures,
hasFailures: failures.length > 0,
};
}
async updateCartItem(itemId, quantity) {
if (quantity <= 0) {
return this.api.delete(`/cart/items/${itemId}`);
}
return this.api.put(`/cart/items/${itemId}`, { quantity });
}
}
// 使用示例
const cartService = new ShoppingCartService(api);
async function handleAddToCart(productId, quantity) {
const [error, result] = await ErrorUtils.to(
cartService.addToCart(productId, quantity)
);
if (error) {
switch (error.code) {
case 'STOCK_CHECK_FAILED':
showMessage('网络异常,请稍后重试', 'error');
break;
case 'PRODUCT_UNAVAILABLE':
showMessage('商品已售罄', 'warning');
break;
case 'ADD_TO_CART_FAILED':
showMessage('添加失败,请重试', 'error');
break;
default:
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else {
showMessage('操作失败', 'error');
}
}
return;
}
showMessage('添加成功', 'success');
updateCartBadge(result.totalItems);
}
2. 文件上传进度场景
javascript
// 文件上传的错误处理和进度跟踪
class FileUploadService {
constructor() {
this.activeUploads = new Map();
}
async uploadFile(file, options = {}) {
const uploadId = this.generateUploadId();
const {
onProgress,
onSuccess,
onError,
maxSize = 50 * 1024 * 1024, // 50MB
allowedTypes = ['image/*', 'application/pdf'],
} = options;
// 文件验证
const validationResult = this.validateFile(file, { maxSize, allowedTypes });
if (!validationResult.valid) {
const error = new ValidationError(validationResult.message, 'file');
onError?.(error);
return Result.error(error);
}
try {
// 1. 获取上传凭证
const [credError, credentials] = await ErrorUtils.to(
this.getUploadCredentials(file.name, file.size, file.type)
);
if (credError) {
throw new AppError('获取上传凭证失败', 'CREDENTIALS_FAILED');
}
// 2. 分片上传
const chunks = this.createFileChunks(file);
const uploadedChunks = [];
this.activeUploads.set(uploadId, {
file,
chunks: chunks.length,
uploaded: 0,
status: 'uploading',
});
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
// 重试机制
const [chunkError, chunkResult] = await ErrorUtils.retry(
() => this.uploadChunk(credentials.uploadUrl, chunk, i),
3,
1000
);
if (chunkError) {
this.activeUploads.delete(uploadId);
throw new AppError(`分片 ${i + 1} 上传失败`, 'CHUNK_UPLOAD_FAILED');
}
uploadedChunks.push(chunkResult);
// 更新进度
const progress = Math.round(((i + 1) / chunks.length) * 100);
onProgress?.({
progress,
uploadId,
chunk: i + 1,
total: chunks.length,
});
this.activeUploads.get(uploadId).uploaded = i + 1;
}
// 3. 合并文件
const [mergeError, finalResult] = await ErrorUtils.to(
this.mergeChunks(credentials.fileId, uploadedChunks)
);
if (mergeError) {
throw new AppError('文件合并失败', 'MERGE_FAILED');
}
this.activeUploads.delete(uploadId);
onSuccess?.(finalResult);
return Result.ok(finalResult);
} catch (error) {
this.activeUploads.delete(uploadId);
onError?.(error);
return Result.error(error);
}
}
validateFile(file, { maxSize, allowedTypes }) {
if (!file) {
return { valid: false, message: '请选择文件' };
}
if (file.size > maxSize) {
const sizeMB = Math.round(maxSize / 1024 / 1024);
return { valid: false, message: `文件大小不能超过 ${sizeMB}MB` };
}
const isValidType = allowedTypes.some((type) => {
if (type.endsWith('/*')) {
return file.type.startsWith(type.slice(0, -1));
}
return file.type === type;
});
if (!isValidType) {
return { valid: false, message: '文件类型不支持' };
}
return { valid: true };
}
async getUploadCredentials(fileName, fileSize, fileType) {
return api.post('/upload/credentials', {
fileName,
fileSize,
fileType,
});
}
createFileChunks(file, chunkSize = 2 * 1024 * 1024) {
// 2MB per chunk
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size);
chunks.push(file.slice(start, end));
start = end;
}
return chunks;
}
async uploadChunk(uploadUrl, chunk, index) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
return fetch(uploadUrl, {
method: 'POST',
body: formData,
}).then((r) => r.json());
}
async mergeChunks(fileId, chunks) {
return api.post(`/upload/${fileId}/merge`, {
chunks: chunks.map((c) => c.chunkId),
});
}
generateUploadId() {
return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 取消上传
cancelUpload(uploadId) {
if (this.activeUploads.has(uploadId)) {
this.activeUploads.get(uploadId).status = 'cancelled';
this.activeUploads.delete(uploadId);
}
}
// 获取上传状态
getUploadStatus(uploadId) {
return this.activeUploads.get(uploadId) || null;
}
}
// Vue 3 组合式函数
export function useFileUpload() {
const uploadService = new FileUploadService();
const uploads = ref(new Map());
const uploadFile = async (file, options = {}) => {
const uploadId = `upload_${Date.now()}`;
uploads.value.set(uploadId, {
file,
progress: 0,
status: 'uploading',
error: null,
result: null,
});
const result = await uploadService.uploadFile(file, {
...options,
onProgress: ({ progress }) => {
const upload = uploads.value.get(uploadId);
if (upload) {
upload.progress = progress;
}
},
onSuccess: (result) => {
const upload = uploads.value.get(uploadId);
if (upload) {
upload.status = 'completed';
upload.result = result;
}
},
onError: (error) => {
const upload = uploads.value.get(uploadId);
if (upload) {
upload.status = 'error';
upload.error = error;
}
},
});
return { uploadId, result };
};
const cancelUpload = (uploadId) => {
uploadService.cancelUpload(uploadId);
uploads.value.delete(uploadId);
};
return {
uploads: readonly(uploads),
uploadFile,
cancelUpload,
};
}
3. 数据同步和缓存场景
javascript
// 复杂的数据同步和缓存错误处理
class DataSyncService {
constructor() {
this.cache = new Map();
this.syncQueue = [];
this.isSyncing = false;
this.retryAttempts = new Map();
}
// 获取数据(带缓存)
async getData(key, fetcher, options = {}) {
const {
cacheTime = 5 * 60 * 1000, // 5分钟缓存
forceRefresh = false,
fallbackData = null,
} = options;
// 检查缓存
if (!forceRefresh && this.cache.has(key)) {
const cached = this.cache.get(key);
if (Date.now() - cached.timestamp < cacheTime) {
return Result.ok(cached.data);
}
}
// 获取新数据
const [error, data] = await ErrorUtils.to(fetcher());
if (error) {
// 如果有缓存数据,返回缓存
if (this.cache.has(key)) {
const cached = this.cache.get(key);
console.warn(`获取 ${key} 失败,使用缓存数据:`, error);
return Result.ok(cached.data);
}
// 如果有降级数据,返回降级数据
if (fallbackData !== null) {
console.warn(`获取 ${key} 失败,使用降级数据:`, error);
return Result.ok(fallbackData);
}
return Result.error(error);
}
// 更新缓存
this.cache.set(key, {
data,
timestamp: Date.now(),
});
return Result.ok(data);
}
// 离线数据同步
async syncOfflineData() {
if (this.isSyncing) {
return { success: false, message: '正在同步中' };
}
this.isSyncing = true;
const results = {
success: [],
failed: [],
skipped: [],
};
try {
// 检查网络状态
if (!navigator.onLine) {
throw new AppError('网络不可用', 'NETWORK_UNAVAILABLE');
}
// 处理同步队列
while (this.syncQueue.length > 0) {
const item = this.syncQueue.shift();
const retryKey = `${item.type}_${item.id}`;
const retryCount = this.retryAttempts.get(retryKey) || 0;
// 超过重试次数,跳过
if (retryCount >= 3) {
results.skipped.push({
...item,
reason: '超过最大重试次数',
});
this.retryAttempts.delete(retryKey);
continue;
}
const [error, result] = await ErrorUtils.to(this.syncSingleItem(item));
if (error) {
// 增加重试次数
this.retryAttempts.set(retryKey, retryCount + 1);
// 如果是临时性错误,重新加入队列
if (this.isRetryableError(error)) {
this.syncQueue.push(item);
}
results.failed.push({
...item,
error: error.message,
retryCount: retryCount + 1,
});
} else {
results.success.push({
...item,
result,
});
this.retryAttempts.delete(retryKey);
}
// 避免过快的请求
await new Promise((resolve) => setTimeout(resolve, 100));
}
} finally {
this.isSyncing = false;
}
return results;
}
async syncSingleItem(item) {
switch (item.type) {
case 'user_preference':
return api.put('/user/preferences', item.data);
case 'analytics_event':
return api.post('/analytics/events', item.data);
case 'draft_save':
return api.post('/drafts', item.data);
default:
throw new AppError(`未知的同步类型: ${item.type}`, 'UNKNOWN_SYNC_TYPE');
}
}
isRetryableError(error) {
// 网络错误、服务器临时错误等可重试
return (
error instanceof NetworkError ||
(error.statusCode >= 500 && error.statusCode < 600) ||
error.statusCode === 429
); // 限流
}
// 添加到同步队列
addToSyncQueue(type, id, data) {
this.syncQueue.push({
type,
id,
data,
timestamp: Date.now(),
});
}
// 清理过期缓存
cleanExpiredCache(maxAge = 30 * 60 * 1000) {
// 30分钟
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
if (now - value.timestamp > maxAge) {
this.cache.delete(key);
}
}
}
}
// 使用示例
const syncService = new DataSyncService();
// 获取用户数据(带缓存和降级)
async function fetchUserProfile(userId) {
const result = await syncService.getData(
`user_profile_${userId}`,
() => api.get(`/users/${userId}`),
{
cacheTime: 10 * 60 * 1000, // 10分钟缓存
fallbackData: {
id: userId,
name: '用户',
avatar: '/default-avatar.png',
},
}
);
if (result.isError()) {
console.error('获取用户资料失败:', result.error);
return null;
}
return result.data;
}
// 离线操作处理
async function saveUserPreference(key, value) {
const [error, result] = await ErrorUtils.to(
api.put('/user/preferences', { [key]: value })
);
if (error) {
// 离线时保存到同步队列
if (!navigator.onLine || error instanceof NetworkError) {
syncService.addToSyncQueue('user_preference', key, { [key]: value });
console.log('已添加到离线同步队列');
return true;
}
throw error;
}
return result;
}
4. 实时数据流处理场景
javascript
// WebSocket 连接和实时数据的错误处理
class RealtimeDataService {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.listeners = new Map();
this.messageQueue = [];
this.isConnecting = false;
}
async connect() {
if (this.isConnecting) {
return;
}
this.isConnecting = true;
try {
await this.createConnection();
this.reconnectAttempts = 0;
this.isConnecting = false;
} catch (error) {
this.isConnecting = false;
throw error;
}
}
async createConnection() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 连接成功');
this.processMessageQueue();
resolve();
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onclose = (event) => {
console.log('WebSocket 连接关闭:', event.code, event.reason);
this.handleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
reject(new NetworkError('WebSocket 连接失败'));
};
// 连接超时
setTimeout(() => {
if (this.ws.readyState === WebSocket.CONNECTING) {
this.ws.close();
reject(new NetworkError('WebSocket 连接超时'));
}
}, 10000);
} catch (error) {
reject(error);
}
});
}
async handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.emit('maxReconnectAttemptsReached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(
`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts}),${delay}ms 后重试`
);
setTimeout(async () => {
const [error] = await ErrorUtils.to(this.connect());
if (error) {
console.error('重连失败:', error);
this.handleReconnect();
}
}, delay);
}
handleMessage(data) {
try {
const message = JSON.parse(data);
this.emit('message', message);
// 根据消息类型分发
if (message.type) {
this.emit(message.type, message.payload);
}
} catch (error) {
console.error('解析消息失败:', error, data);
this.emit('parseError', { error, rawData: data });
}
}
send(data) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
return true;
} else {
// 连接未就绪时加入队列
this.messageQueue.push(data);
// 尝试连接
if (!this.isConnecting) {
this.connect().catch((error) => {
console.error('发送消息时连接失败:', error);
});
}
return false;
}
}
processMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.send(message);
}
}
on(event, listener) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(listener);
}
off(event, listener) {
if (this.listeners.has(event)) {
this.listeners.get(event).delete(listener);
}
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach((listener) => {
try {
listener(data);
} catch (error) {
console.error(`事件监听器 ${event} 执行失败:`, error);
}
});
}
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.reconnectAttempts = this.maxReconnectAttempts; // 阻止重连
}
}
// 实时数据管理器
class LiveDataManager {
constructor() {
this.connections = new Map();
this.subscriptions = new Map();
}
async subscribe(channel, handler, options = {}) {
const { autoReconnect = true, maxRetries = 5, retryDelay = 1000 } = options;
let connection = this.connections.get(channel);
if (!connection) {
connection = new RealtimeDataService(
`wss://api.example.com/live/${channel}`
);
this.connections.set(channel, connection);
// 设置错误处理
connection.on('maxReconnectAttemptsReached', () => {
this.handleConnectionFailed(channel);
});
connection.on('parseError', ({ error, rawData }) => {
console.error(`频道 ${channel} 消息解析错误:`, error);
});
}
// 添加订阅处理器
const subscriptionId = `${channel}_${Date.now()}_${Math.random()}`;
this.subscriptions.set(subscriptionId, {
channel,
handler,
connection,
});
connection.on('message', handler);
// 连接
const [connectError] = await ErrorUtils.to(connection.connect());
if (connectError) {
this.subscriptions.delete(subscriptionId);
throw new AppError(`订阅频道 ${channel} 失败`, 'SUBSCRIPTION_FAILED');
}
return subscriptionId;
}
unsubscribe(subscriptionId) {
const subscription = this.subscriptions.get(subscriptionId);
if (subscription) {
subscription.connection.off('message', subscription.handler);
this.subscriptions.delete(subscriptionId);
// 如果没有其他订阅,关闭连接
const hasOtherSubscriptions = Array.from(
this.subscriptions.values()
).some((sub) => sub.channel === subscription.channel);
if (!hasOtherSubscriptions) {
subscription.connection.disconnect();
this.connections.delete(subscription.channel);
}
}
}
handleConnectionFailed(channel) {
console.error(`频道 ${channel} 连接失败,已达到最大重试次数`);
// 通知所有该频道的订阅者
const channelSubscriptions = Array.from(this.subscriptions.values()).filter(
(sub) => sub.channel === channel
);
channelSubscriptions.forEach((sub) => {
try {
sub.handler({
type: 'connection_failed',
channel,
message: '连接失败,请稍后重试',
});
} catch (error) {
console.error('通知订阅者连接失败时出错:', error);
}
});
}
// 获取连接状态
getConnectionStatus(channel) {
const connection = this.connections.get(channel);
if (!connection || !connection.ws) {
return 'disconnected';
}
switch (connection.ws.readyState) {
case WebSocket.CONNECTING:
return 'connecting';
case WebSocket.OPEN:
return 'connected';
case WebSocket.CLOSING:
return 'closing';
case WebSocket.CLOSED:
return 'closed';
default:
return 'unknown';
}
}
}
// Vue 3 实时数据 Hook
export function useLiveData(channel, options = {}) {
const dataManager = new LiveDataManager();
const data = ref(null);
const error = ref(null);
const status = ref('disconnected');
const subscriptionId = ref(null);
const subscribe = async () => {
try {
error.value = null;
status.value = 'connecting';
const id = await dataManager.subscribe(
channel,
(message) => {
if (message.type === 'connection_failed') {
error.value = new Error(message.message);
status.value = 'error';
} else {
data.value = message;
status.value = 'connected';
}
},
options
);
subscriptionId.value = id;
status.value = 'connected';
} catch (err) {
error.value = err;
status.value = 'error';
}
};
const unsubscribe = () => {
if (subscriptionId.value) {
dataManager.unsubscribe(subscriptionId.value);
subscriptionId.value = null;
status.value = 'disconnected';
}
};
onMounted(() => {
subscribe();
});
onUnmounted(() => {
unsubscribe();
});
return {
data: readonly(data),
error: readonly(error),
status: readonly(status),
subscribe,
unsubscribe,
reconnect: subscribe,
};
}
5. 表单验证和提交场景
javascript
// 复杂表单的错误处理
class FormValidator {
constructor() {
this.rules = new Map();
this.errors = new Map();
}
addRule(field, validator, message) {
if (!this.rules.has(field)) {
this.rules.set(field, []);
}
this.rules.get(field).push({ validator, message });
return this;
}
async validate(data) {
this.errors.clear();
const validationPromises = [];
for (const [field, rules] of this.rules) {
const fieldPromise = this.validateField(field, data[field], data);
validationPromises.push(fieldPromise);
}
await Promise.all(validationPromises);
return {
valid: this.errors.size === 0,
errors: Object.fromEntries(this.errors),
};
}
async validateField(field, value, allData) {
const rules = this.rules.get(field) || [];
for (const rule of rules) {
try {
const isValid = await rule.validator(value, allData);
if (!isValid) {
this.errors.set(field, rule.message);
break; // 只显示第一个错误
}
} catch (error) {
this.errors.set(field, `验证失败: ${error.message}`);
break;
}
}
}
clearErrors() {
this.errors.clear();
}
getFieldError(field) {
return this.errors.get(field);
}
}
// 表单提交服务
class FormSubmissionService {
constructor() {
this.submitting = new Set();
}
async submitForm(formId, data, options = {}) {
if (this.submitting.has(formId)) {
throw new AppError('表单正在提交中', 'FORM_SUBMITTING');
}
const {
validator,
beforeSubmit,
afterSubmit,
onSuccess,
onError,
endpoint,
method = 'POST',
} = options;
this.submitting.add(formId);
try {
// 1. 表单验证
if (validator) {
const validation = await validator.validate(data);
if (!validation.valid) {
throw new ValidationError('表单验证失败', validation.errors);
}
}
// 2. 提交前处理
let processedData = data;
if (beforeSubmit) {
const [beforeError, result] = await ErrorUtils.to(beforeSubmit(data));
if (beforeError) {
throw new AppError('提交前处理失败', 'BEFORE_SUBMIT_FAILED');
}
processedData = result || data;
}
// 3. 提交数据
const [submitError, response] = await ErrorUtils.to(
api.request(endpoint, {
method,
body: JSON.stringify(processedData),
})
);
if (submitError) {
// 处理特定的提交错误
if (submitError.statusCode === 422) {
// 服务器验证错误
const validationErrors = submitError.data?.errors || {};
throw new ValidationError('服务器验证失败', validationErrors);
}
throw new AppError('提交失败,请重试', 'SUBMIT_FAILED');
}
// 4. 提交后处理
if (afterSubmit) {
await ErrorUtils.to(afterSubmit(response));
}
onSuccess?.(response);
return Result.ok(response);
} catch (error) {
onError?.(error);
return Result.error(error);
} finally {
this.submitting.delete(formId);
}
}
isSubmitting(formId) {
return this.submitting.has(formId);
}
}
// 使用示例:用户注册表单
async function setupUserRegistrationForm() {
const validator = new FormValidator()
.addRule(
'username',
async (value) => {
if (!value || value.length < 3) return false;
// 检查用户名是否已存在
const [error, result] = await ErrorUtils.to(
api.get(`/users/check-username?username=${encodeURIComponent(value)}`)
);
return !error && result.available;
},
'用户名至少3位且不能重复'
)
.addRule(
'email',
(value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
},
'请输入有效的邮箱地址'
)
.addRule(
'password',
(value) => {
return (
value &&
value.length >= 8 &&
/[A-Z]/.test(value) &&
/[0-9]/.test(value)
);
},
'密码至少8位,包含大写字母和数字'
)
.addRule(
'confirmPassword',
(value, allData) => {
return value === allData.password;
},
'两次密码输入不一致'
);
const formService = new FormSubmissionService();
const submitRegistration = async (formData) => {
const result = await formService.submitForm('registration', formData, {
validator,
endpoint: '/users/register',
beforeSubmit: async (data) => {
// 添加时间戳和设备信息
return {
...data,
timestamp: Date.now(),
deviceInfo: {
userAgent: navigator.userAgent,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
};
},
afterSubmit: async (response) => {
// 注册成功后的处理
if (response.user) {
localStorage.setItem('user', JSON.stringify(response.user));
// 发送欢迎邮件
await ErrorUtils.to(
api.post('/users/send-welcome-email', { userId: response.user.id })
);
}
},
onSuccess: (response) => {
showMessage('注册成功!', 'success');
router.push('/dashboard');
},
onError: (error) => {
if (error instanceof ValidationError) {
// 显示字段验证错误
Object.entries(error.field || {}).forEach(([field, message]) => {
showFieldError(field, message);
});
} else {
showMessage(error.message || '注册失败,请重试', 'error');
}
},
});
return result;
};
return {
validator,
submitRegistration,
isSubmitting: (formId) => formService.isSubmitting(formId),
};
}
🚀 性能优化和测试实例
1. 性能监控和错误追踪
javascript
// 性能监控的错误处理
class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.errorTracker = new Map();
}
// 监控异步操作性能
async monitorAsyncOperation(name, operation, options = {}) {
const {
timeout = 30000,
expectSuccessRate = 0.95,
alertThreshold = 1000,
} = options;
const startTime = performance.now();
const operationId = `${name}_${Date.now()}`;
try {
// 添加超时控制
const result = await ErrorUtils.withTimeout(operation(), timeout);
const duration = performance.now() - startTime;
this.recordSuccess(name, duration);
// 性能告警
if (duration > alertThreshold) {
console.warn(`操作 ${name} 耗时过长: ${duration}ms`);
this.reportSlowOperation(name, duration, operationId);
}
return Result.ok(result);
} catch (error) {
const duration = performance.now() - startTime;
this.recordError(name, error, duration);
// 检查错误率
const stats = this.getOperationStats(name);
if (stats.errorRate > 1 - expectSuccessRate) {
this.alertHighErrorRate(name, stats);
}
return Result.error(error);
}
}
recordSuccess(operationName, duration) {
const key = `${operationName}_success`;
const records = this.metrics.get(key) || [];
records.push({
timestamp: Date.now(),
duration,
status: 'success',
});
// 只保留最近1000条记录
if (records.length > 1000) {
records.shift();
}
this.metrics.set(key, records);
}
recordError(operationName, error, duration) {
const key = `${operationName}_error`;
const records = this.errorTracker.get(key) || [];
records.push({
timestamp: Date.now(),
duration,
error: {
message: error.message,
code: error.code,
stack: error.stack,
type: error.constructor.name,
},
status: 'error',
});
if (records.length > 1000) {
records.shift();
}
this.errorTracker.set(key, records);
}
getOperationStats(operationName) {
const successKey = `${operationName}_success`;
const errorKey = `${operationName}_error`;
const successes = this.metrics.get(successKey) || [];
const errors = this.errorTracker.get(errorKey) || [];
const total = successes.length + errors.length;
const errorRate = total > 0 ? errors.length / total : 0;
const durations = successes.map((r) => r.duration);
const avgDuration =
durations.length > 0
? durations.reduce((a, b) => a + b, 0) / durations.length
: 0;
const p95Duration =
durations.length > 0
? durations.sort((a, b) => a - b)[Math.floor(durations.length * 0.95)]
: 0;
return {
total,
successes: successes.length,
errors: errors.length,
errorRate,
avgDuration,
p95Duration,
recentErrors: errors.slice(-5), // 最近5个错误
};
}
async reportSlowOperation(name, duration, operationId) {
// 上报慢操作
const [error] = await ErrorUtils.to(
api.post('/monitoring/slow-operations', {
operationName: name,
duration,
operationId,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
})
);
if (error) {
console.warn('上报慢操作失败:', error);
}
}
async alertHighErrorRate(name, stats) {
console.error(`操作 ${name} 错误率过高:`, stats);
// 发送告警
const [error] = await ErrorUtils.to(
api.post('/monitoring/alerts', {
type: 'high_error_rate',
operationName: name,
errorRate: stats.errorRate,
recentErrors: stats.recentErrors,
timestamp: Date.now(),
})
);
if (error) {
console.warn('发送告警失败:', error);
}
}
// 生成性能报告
generateReport() {
const operations = new Set();
// 收集所有操作名称
this.metrics.forEach((_, key) => {
const operationName = key.replace('_success', '');
operations.add(operationName);
});
this.errorTracker.forEach((_, key) => {
const operationName = key.replace('_error', '');
operations.add(operationName);
});
const report = {};
operations.forEach((name) => {
report[name] = this.getOperationStats(name);
});
return report;
}
}
// 全局性能监控实例
const performanceMonitor = new PerformanceMonitor();
// 装饰器:自动性能监控
function withPerformanceMonitoring(operationName, options = {}) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
return await performanceMonitor.monitorAsyncOperation(
`${target.constructor.name}.${propertyKey}`,
() => originalMethod.apply(this, args),
options
);
};
return descriptor;
};
}
// 使用示例
class DataService {
@withPerformanceMonitoring('fetchUserData', {
timeout: 5000,
alertThreshold: 2000,
})
async fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
@withPerformanceMonitoring('batchUpdateUsers', {
timeout: 30000,
expectSuccessRate: 0.98,
})
async batchUpdateUsers(updates) {
const response = await fetch('/api/users/batch', {
method: 'PUT',
body: JSON.stringify(updates),
});
return response.json();
}
}
2. 错误处理的单元测试
javascript
// 测试工具类
class AsyncTestUtils {
// 模拟异步错误
static async simulateAsyncError(errorType = 'network', delay = 100) {
await new Promise((resolve) => setTimeout(resolve, delay));
switch (errorType) {
case 'network':
throw new NetworkError('模拟网络错误');
case 'validation':
throw new ValidationError('模拟验证错误', 'testField');
case 'timeout':
throw new Error('操作超时');
default:
throw new Error('模拟通用错误');
}
}
// 模拟成功响应
static async simulateAsyncSuccess(data = { success: true }, delay = 100) {
await new Promise((resolve) => setTimeout(resolve, delay));
return data;
}
// 模拟间歇性失败
static async simulateIntermittentFailure(successRate = 0.7) {
const random = Math.random();
if (random < successRate) {
return this.simulateAsyncSuccess();
} else {
return this.simulateAsyncError();
}
}
}
// Jest 测试示例
describe('ErrorUtils.to() 函数测试', () => {
test('应该正确处理成功的 Promise', async () => {
const [error, data] = await ErrorUtils.to(
AsyncTestUtils.simulateAsyncSuccess({ value: 42 })
);
expect(error).toBeNull();
expect(data).toEqual({ value: 42 });
});
test('应该正确处理失败的 Promise', async () => {
const [error, data] = await ErrorUtils.to(
AsyncTestUtils.simulateAsyncError('network')
);
expect(error).toBeInstanceOf(NetworkError);
expect(data).toBeNull();
});
test('应该处理超时错误', async () => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('超时')), 50);
});
const [error, data] = await ErrorUtils.to(timeoutPromise);
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('超时');
expect(data).toBeNull();
});
});
describe('ErrorUtils.retry() 重试功能测试', () => {
test('应该在最终成功时返回结果', async () => {
let attempts = 0;
const flaky = async () => {
attempts++;
if (attempts < 3) {
throw new Error('失败');
}
return { success: true, attempts };
};
const [error, result] = await ErrorUtils.retry(flaky, 3, 10);
expect(error).toBeNull();
expect(result).toEqual({ success: true, attempts: 3 });
});
test('应该在达到最大重试次数后返回错误', async () => {
const alwaysFails = async () => {
throw new Error('总是失败');
};
const [error, result] = await ErrorUtils.retry(alwaysFails, 2, 10);
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('总是失败');
expect(result).toBeNull();
});
});
describe('Result 包装器测试', () => {
test('Result.ok() 应该创建成功结果', () => {
const result = Result.ok('test data');
expect(result.isOk()).toBe(true);
expect(result.isError()).toBe(false);
expect(result.unwrap()).toBe('test data');
});
test('Result.error() 应该创建错误结果', () => {
const error = new Error('test error');
const result = Result.error(error);
expect(result.isOk()).toBe(false);
expect(result.isError()).toBe(true);
expect(() => result.unwrap()).toThrow('test error');
});
test('Result.from() 应该正确包装 Promise', async () => {
const successResult = await Result.from(
AsyncTestUtils.simulateAsyncSuccess('success')
);
expect(successResult.isOk()).toBe(true);
expect(successResult.unwrap()).toBe('success');
const errorResult = await Result.from(AsyncTestUtils.simulateAsyncError());
expect(errorResult.isError()).toBe(true);
});
test('map() 应该正确转换成功值', () => {
const result = Result.ok(5);
const mapped = result.map((x) => x * 2);
expect(mapped.unwrap()).toBe(10);
});
test('map() 应该跳过错误值', () => {
const result = Result.error(new Error('test'));
const mapped = result.map((x) => x * 2);
expect(mapped.isError()).toBe(true);
});
});
3. 集成测试和 E2E 测试
javascript
// Cypress E2E 测试示例
describe('错误处理集成测试', () => {
beforeEach(() => {
// 模拟网络错误
cy.intercept('GET', '/api/users/*', { forceNetworkError: true }).as(
'userError'
);
cy.visit('/user-profile');
});
it('应该优雅地处理网络错误', () => {
// 等待错误处理
cy.wait('@userError');
// 验证错误消息显示
cy.contains('网络异常,请稍后重试').should('be.visible');
// 验证重试按钮可用
cy.get('[data-testid="retry-button"]').should('be.enabled');
// 验证降级内容显示
cy.get('[data-testid="fallback-content"]').should('be.visible');
});
it('应该在网络恢复后自动重试', () => {
// 模拟网络恢复
cy.intercept('GET', '/api/users/*', { fixture: 'user.json' }).as(
'userSuccess'
);
// 点击重试
cy.get('[data-testid="retry-button"]').click();
cy.wait('@userSuccess');
// 验证数据正确显示
cy.get('[data-testid="user-name"]').should('contain', 'John Doe');
cy.get('[data-testid="error-message"]').should('not.exist');
});
});
// Playwright 测试示例
import { test, expect } from '@playwright/test';
test.describe('异步错误处理', () => {
test('文件上传错误处理', async ({ page }) => {
await page.goto('/upload');
// 模拟服务器错误
await page.route('/api/upload/**', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: '服务器内部错误' }),
});
});
// 上传文件
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('./test-files/sample.pdf');
// 验证错误处理
await expect(page.locator('[data-testid="upload-error"]')).toBeVisible();
await expect(page.locator('[data-testid="upload-error"]')).toContainText(
'上传失败'
);
// 验证重试功能
await page.locator('[data-testid="retry-upload"]').click();
await expect(page.locator('[data-testid="upload-progress"]')).toBeVisible();
});
test('实时数据连接错误处理', async ({ page }) => {
// 模拟 WebSocket 连接失败
await page.addInitScript(() => {
window.WebSocket = class {
constructor() {
setTimeout(() => {
this.onerror && this.onerror(new Event('error'));
}, 100);
}
};
});
await page.goto('/dashboard');
// 验证错误状态显示
await expect(
page.locator('[data-testid="connection-status"]')
).toContainText('连接失败');
await expect(
page.locator('[data-testid="reconnect-button"]')
).toBeVisible();
// 验证离线模式
await expect(
page.locator('[data-testid="offline-indicator"]')
).toBeVisible();
});
});
4. 性能基准测试
javascript
// 性能基准测试
class PerformanceBenchmark {
static async benchmarkErrorHandling() {
const iterations = 1000;
const results = {};
// 测试传统 try-catch
console.time('traditional-try-catch');
for (let i = 0; i < iterations; i++) {
try {
await AsyncTestUtils.simulateIntermittentFailure(0.8);
} catch (error) {
// 处理错误
}
}
console.timeEnd('traditional-try-catch');
// 测试 to() 函数
console.time('to-function');
for (let i = 0; i < iterations; i++) {
const [error, result] = await ErrorUtils.to(
AsyncTestUtils.simulateIntermittentFailure(0.8)
);
}
console.timeEnd('to-function');
// 测试 Result 包装器
console.time('result-wrapper');
for (let i = 0; i < iterations; i++) {
const result = await Result.from(
AsyncTestUtils.simulateIntermittentFailure(0.8)
);
}
console.timeEnd('result-wrapper');
// 内存使用情况
if (performance.memory) {
results.memoryUsage = {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit,
};
}
return results;
}
static async stressTest() {
const concurrentOperations = 100;
const operations = Array.from({ length: concurrentOperations }, (_, i) =>
ErrorUtils.to(AsyncTestUtils.simulateIntermittentFailure(0.7))
);
const startTime = performance.now();
const results = await Promise.all(operations);
const endTime = performance.now();
const successes = results.filter(([error]) => !error).length;
const failures = results.filter(([error]) => error).length;
return {
duration: endTime - startTime,
totalOperations: concurrentOperations,
successes,
failures,
successRate: successes / concurrentOperations,
opsPerSecond: concurrentOperations / ((endTime - startTime) / 1000),
};
}
}
// 运行基准测试
async function runBenchmarks() {
console.log('开始性能基准测试...');
const errorHandlingBenchmark =
await PerformanceBenchmark.benchmarkErrorHandling();
console.log('错误处理基准测试结果:', errorHandlingBenchmark);
const stressTestResult = await PerformanceBenchmark.stressTest();
console.log('压力测试结果:', stressTestResult);
// 生成报告
const report = {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
errorHandling: errorHandlingBenchmark,
stressTest: stressTestResult,
};
// 保存或上传报告
localStorage.setItem('performance-report', JSON.stringify(report));
return report;
}
5. 错误边界和全局错误处理
javascript
// React 错误边界
class AsyncErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo,
});
// 异步错误上报
this.reportError(error, errorInfo);
}
async reportError(error, errorInfo) {
const [reportError] = await ErrorUtils.to(
api.post('/errors/report', {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
})
);
if (reportError) {
console.warn('错误上报失败:', reportError);
}
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="error-boundary">
<h2>出现了一些问题</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
<button
onClick={() => this.setState({ hasError: false, error: null })}
>
重试
</button>
</div>
)
);
}
return this.props.children;
}
}
// Vue 3 全局错误处理
const app = createApp(App);
app.config.errorHandler = async (error, instance, info) => {
console.error('Vue 全局错误:', error, info);
// 异步错误上报
const [reportError] = await ErrorUtils.to(
api.post('/errors/vue-report', {
message: error.message,
stack: error.stack,
componentName: instance?.$options.name || 'Unknown',
errorInfo: info,
timestamp: Date.now(),
route: instance?.$route?.path,
})
);
if (reportError) {
console.warn('Vue 错误上报失败:', reportError);
}
};
// 全局未捕获错误处理
window.addEventListener('error', async (event) => {
const [reportError] = await ErrorUtils.to(
api.post('/errors/global', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
})
);
if (reportError) {
console.warn('全局错误上报失败:', reportError);
}
});
// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', async (event) => {
console.error('未处理的 Promise 拒绝:', event.reason);
const [reportError] = await ErrorUtils.to(
api.post('/errors/unhandled-promise', {
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
})
);
if (reportError) {
console.warn('Promise 错误上报失败:', reportError);
}
// 阻止控制台错误输出
event.preventDefault();
});
📊 错误处理最佳实践总结
1. 选择指南
场景 | 推荐方案 | 原因 |
---|---|---|
简单异步操作 | to() 函数 |
语法简洁,易于理解 |
复杂业务逻辑 | Result 包装器 | 类型安全,链式操作 |
重复性操作 | 装饰器模式 | 代码复用,统一处理 |
表单验证 | 专用验证器 | 业务专一,错误详细 |
实时数据 | 连接管理器 | 自动重连,状态管理 |
性能敏感 | 性能监控 | 及时发现,主动优化 |
2. 性能对比
方案 | 内存开销 | 执行速度 | 代码复杂度 | 可维护性 |
---|---|---|---|---|
try-catch | 低 | 快 | 中 | 低 |
to() 函数 | 低 | 快 | 低 | 高 |
Result 包装器 | 中 | 中 | 中 | 高 |
装饰器模式 | 中 | 中 | 高 | 高 |
3. 团队采用建议
- 渐进式采用 :从
to()
函数开始,逐步引入其他模式 - 统一规范:制定团队编码规范,保持一致性
- 工具支持:配置 ESLint 规则,IDE 代码片段
- 培训推广:团队分享,代码审查
- 监控反馈:收集使用反馈,持续优化
本文档持续更新中,欢迎贡献更多优雅的错误处理模式!