优雅处理JS异步编程中的细粒度错误
在JavaScript异步编程中,错误处理一直是个棘手的问题。传统的try-catch只能捕获同步错误,而对于Promise、async/await等异步操作,我们需要更精细的错误处理机制。
传统错误处理方式的局限性
让我们先看一个典型的错误处理场景:
// 传统的错误处理方式
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('获取用户数据失败:', error);
// 这里丢失了具体的错误类型和上下文信息
return null;
});
}这种方式存在几个问题:
- 错误信息过于笼统,难以定位具体问题
- 无法区分不同类型的错误
- 错误处理与业务逻辑耦合严重
- 难以实现统一的错误上报和处理策略
细粒度错误处理的核心思路
细粒度错误处理的核心在于:
- 区分不同类型的错误
- 保留完整的错误上下文
- 实现统一的错误处理策略
- 提供友好的错误信息给用户
实现方案
1. 自定义错误类型
首先定义不同类型的错误,便于后续分类处理:
// 自定义错误类型
class NetworkError extends Error {
constructor(message, originalError = null) {
super(message);
this.name = 'NetworkError';
this.originalError = originalError;
}
}
class ValidationError extends Error {
constructor(message, field = null) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class BusinessLogicError extends Error {
constructor(message, code = null) {
super(message);
this.name = 'BusinessLogicError';
this.code = code;
}
}2. 错误包装器
创建一个错误包装器,用于统一处理和转换错误:
class ErrorHandler {
static handle(error, context = {}) {
// 根据错误类型进行分类处理
if (error.name === 'TypeError' && error.message.includes('fetch')) {
return new NetworkError('网络连接失败', error);
}
if (error.name === 'SyntaxError') {
return new ValidationError('数据格式错误');
}
// 保留原始错误信息,添加上下文
if (error instanceof Error) {
error.context = { ...context, timestamp: Date.now() };
return error;
}
// 未知错误类型
return new Error(`未知错误: ${error}`);
}
static async wrap(promise, context = {}) {
try {
return await promise;
} catch (error) {
throw this.handle(error, context);
}
}
}3. 业务层错误处理
在具体业务逻辑中使用错误处理机制:
class UserService {
async getUserProfile(userId) {
const context = { userId, operation: 'getUserProfile' };
try {
const response = await ErrorHandler.wrap(
fetch(`/api/users/${userId}`),
context
);
if (!response.ok) {
throw new NetworkError(`HTTP ${response.status}: ${response.statusText}`);
}
const userData = await ErrorHandler.wrap(
response.json(),
context
);
// 验证数据格式
this.validateUserData(userData);
return userData;
} catch (error) {
// 根据错误类型进行不同的处理
switch (error.name) {
case 'NetworkError':
// 网络错误,可能需要重试
console.error('网络错误,建议重试:', error);
throw new BusinessLogicError('暂时无法获取用户信息,请稍后重试');
case 'ValidationError':
// 数据验证错误
console.error('数据验证失败:', error);
throw new BusinessLogicError('用户数据格式不正确');
case 'BusinessLogicError':
// 业务逻辑错误,直接抛出
throw error;
default:
// 未知错误
console.error('未知错误:', error);
throw new BusinessLogicError('系统繁忙,请稍后再试');
}
}
}
validateUserData(userData) {
if (!userData || typeof userData !== 'object') {
throw new ValidationError('用户数据必须是对象');
}
if (!userData.id || !userData.name) {
throw new ValidationError('用户数据缺少必要字段', 'userData');
}
}
}4. UI层错误处理
在UI层根据不同的错误类型展示相应的提示:
class UserProfileComponent {
constructor() {
this.userService = new UserService();
}
async loadUserProfile(userId) {
try {
this.showLoading();
const userProfile = await this.userService.getUserProfile(userId);
this.renderUserProfile(userProfile);
} catch (error) {
this.handleError(error);
} finally {
this.hideLoading();
}
}
handleError(error) {
let message = '发生未知错误';
let shouldRetry = false;
switch (error.name) {
case 'BusinessLogicError':
message = error.message;
shouldRetry = error.message.includes('重试');
break;
case 'NetworkError':
message = '网络连接异常,请检查网络设置';
shouldRetry = true;
break;
case 'ValidationError':
message = '数据格式错误';
break;
default:
message = '系统繁忙,请稍后再试';
}
this.showError(message, shouldRetry);
}
showError(message, canRetry = false) {
// 更新UI显示错误信息
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
const retryButton = document.getElementById('retry-button');
retryButton.style.display = canRetry ? 'block' : 'none';
}
}5. 全局错误监控
实现全局错误监控和上报:
class GlobalErrorMonitor {
constructor() {
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
// 监听未捕获的Promise错误
window.addEventListener('unhandledrejection', (event) => {
this.reportError(event.reason);
event.preventDefault();
});
// 监听全局JavaScript错误
window.addEventListener('error', (event) => {
this.reportError(event.error);
});
}
reportError(error) {
// 过滤掉一些已知的无害错误
if (this.shouldIgnoreError(error)) {
return;
}
const errorInfo = {
name: error.name,
message: error.message,
stack: error.stack,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
// 发送错误报告到监控服务
this.sendErrorReport(errorInfo);
}
shouldIgnoreError(error) {
// 忽略特定的错误类型或消息
const ignoredErrors = [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured'
];
return ignoredErrors.some(ignored =>
error.message && error.message.includes(ignored)
);
}
async sendErrorReport(errorInfo) {
try {
await fetch('/api/error-report', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(errorInfo)
});
} catch (reportingError) {
// 错误上报失败不应该影响用户体验
console.warn('错误上报失败:', reportingError);
}
}
}
// 初始化全局错误监控
new GlobalErrorMonitor();最佳实践总结
1. 错误分类要明确
- 网络错误:连接超时、服务器错误等
- 验证错误:数据格式不正确、必填字段缺失等
- 业务逻辑错误:权限不足、资源不存在等
- 系统错误:未知异常、第三方库错误等
2. 错误信息要详细
- 包含错误发生的上下文信息
- 保留原始错误堆栈
- 添加业务相关的错误码
3. 处理策略要分层
- 底层:错误捕获和转换
- 业务层:业务逻辑相关的错误处理
- UI层:用户友好的错误提示
- 监控层:错误统计和上报
4. 用户体验要考虑
- 提供明确的错误提示
- 对于可恢复的错误提供重试机制
- 避免暴露敏感的系统信息
总结
优雅处理JS异步编程中的细粒度错误需要从多个层面入手:通过自定义错误类型明确区分错误种类,使用错误包装器统一处理错误转换,在业务层实现具体的错误处理逻辑,在UI层提供友好的用户提示,最后通过全局监控及时发现和修复问题。
这种多层次的错误处理策略不仅能够提高代码的健壮性和可维护性,还能显著提升用户体验。记住,好的错误处理应该是透明的、可预测的,并且能够帮助开发者快速定位和解决问题。