引言:为什么错误处理如此重要?
在异步编程中,错误处理常常被忽视,但它却是构建健壮应用的关键。想象一下:一个未处理的 Promise 拒绝可能导致整个应用崩溃,而良好的错误处理能提升用户体验并简化调试。本文将深入探讨从基础到高级的错误处理策略。
一、Promise 错误处理基础
1.1 基本的 .catch() 方法
javascript
// 基础用法
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('请求失败:', error));
// 常见陷阱:过早的 .catch()
fetch('/api/data')
.catch(error => console.error('fetch失败')) // ❌ 会捕获所有后续错误
.then(response => response.json()) // 如果fetch失败,这里会继续执行
.then(data => console.log(data));
1.2 Promise 链中的精确错误处理
javascript
function processUserData(userId) {
return fetchUser(userId)
.then(user => {
if (!user.active) {
// 使用 throw 中断 Promise 链
throw new Error('用户未激活');
}
return fetchUserProfile(user.id);
})
.then(profile => {
// 这里只处理 fetchUserProfile 的错误
return transformProfile(profile);
})
.catch(error => {
// 区分不同类型的错误
if (error.message === '用户未激活') {
console.warn('跳过未激活用户');
return { skipped: true, userId };
}
// 重新抛出未知错误
throw error;
});
}
1.3 Promise.all 的错误处理策略
javascript
// 方法1:快速失败(任一失败即整体失败)
async function fetchAllDataQuickFail(urls) {
try {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return await Promise.all(promises);
} catch (error) {
console.error('某个请求失败:', error);
throw error;
}
}
// 方法2:部分成功(使用 Promise.allSettled)
async function fetchAllDataPartialSuccess(urls) {
const promises = urls.map(url =>
fetch(url)
.then(r => r.json())
.catch(error => ({ error, url }))
);
const results = await Promise.allSettled(promises);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (failed.length > 0) {
console.warn(`${failed.length} 个请求失败`);
}
return { successful, failed };
}
二、async/await 错误处理模式
2.1 基本的 try-catch 模式
javascript
async function getUserData(id) {
try {
const user = await fetchUser(id);
const profile = await fetchProfile(user.profileId);
const posts = await fetchUserPosts(user.id);
return { user, profile, posts };
} catch (error) {
// 统一错误处理
console.error(`获取用户 ${id} 数据失败:`, error);
// 返回降级数据
return {
user: { id, name: 'Unknown' },
profile: null,
posts: [],
error: error.message
};
}
}
2.2 更细粒度的错误处理
javascript
async function processOrder(orderId) {
let order, payment, shipping;
try {
order = await fetchOrder(orderId);
} catch (error) {
throw new Error(`订单 ${orderId} 不存在: ${error.message}`);
}
try {
payment = await fetchPayment(order.paymentId);
} catch (error) {
console.warn(`支付信息获取失败,继续处理订单`);
payment = null;
}
try {
shipping = await calculateShipping(order);
} catch (error) {
// 使用默认运费
shipping = { cost: 0, estimatedDays: 7 };
}
return { order, payment, shipping };
}
2.3 避免 try-catch 地狱的实用技巧
javascript
// 技巧1:使用高阶函数封装
function withRetry(fn, retries = 3) {
return async function(...args) {
let lastError;
for (let i = 0; i < retries; i++) {
try {
return await fn(...args);
} catch (error) {
lastError = error;
console.log(`尝试 ${i + 1}/${retries} 失败`);
if (i < retries - 1) {
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
}
throw lastError;
};
}
// 技巧2:使用工具函数处理错误
function safeAwait(promise, fallbackValue = null) {
return promise
.then(data => ({ data, error: null }))
.catch(error => ({ data: fallbackValue, error }));
}
async function fetchDataSafely() {
const { data: users, error: usersError } =
await safeAwait(fetchUsers());
const { data: products, error: productsError } =
await safeAwait(fetchProducts(), []);
if (usersError && productsError) {
throw new Error('所有请求都失败了');
}
return { users: users || [], products };
}
三、高级错误处理模式
3.1 错误边界与错误类型
javascript
// 定义自定义错误类型
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
this.isRetryable = statusCode >= 500;
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// 使用错误类型
async function submitForm(data) {
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
if (!response.ok) {
throw new NetworkError(
'API请求失败',
response.status
);
}
const result = await response.json();
if (result.errors) {
throw new ValidationError(
'数据验证失败',
result.errors[0].field
);
}
return result;
} catch (error) {
// 根据错误类型采取不同策略
switch (error.name) {
case 'NetworkError':
if (error.isRetryable) {
return retryOperation(() => submitForm(data));
}
showToast('网络错误,请检查连接');
break;
case 'ValidationError':
highlightField(error.field);
showToast(`请检查 ${error.field}`);
break;
default:
logErrorToService(error);
showToast('系统错误,请联系管理员');
}
throw error;
}
}
3.2 全局错误处理
javascript
// 在应用入口设置全局 Promise 错误处理器
if (typeof window !== 'undefined') {
// 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', event => {
event.preventDefault();
const { reason } = event;
console.error('未处理的 Promise 拒绝:', reason);
// 发送到错误监控服务
reportErrorToService(reason);
// 用户友好的提示
if (reason instanceof NetworkError) {
showNetworkErrorToast();
}
});
// 全局错误边界(React示例)
class GlobalErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
// 可以在这里重置应用状态或显示错误页面
if (error instanceof NetworkError && error.isRetryable) {
this.setState({ shouldRetry: true });
}
}
render() {
if (this.state.shouldRetry) {
return <RetryButton onClick={this.retry} />;
}
return this.props.children;
}
}
}
3.3 并发与竞态条件的错误处理
javascript
function createCancelablePromise(promise) {
let isCanceled = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
value => !isCanceled && resolve(value),
error => !isCanceled && reject(error)
);
});
return {
promise: wrappedPromise,
cancel: () => { isCanceled = true; }
};
}
async function searchWithDebounce(query) {
// 取消之前的搜索请求
if (this.currentSearch) {
this.currentSearch.cancel();
}
this.currentSearch = createCancelablePromise(
fetch(`/api/search?q=${query}`)
.then(r => r.json())
);
try {
const results = await this.currentSearch.promise;
return results;
} catch (error) {
// 忽略被取消的请求的错误
if (!error.isCanceled) {
throw error;
}
}
}
四、实战:完整的 API 请求封装
javascript
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.pendingRequests = new Map();
}
async request(endpoint, options = {}) {
const requestId = `${endpoint}-${Date.now()}`;
const controller = new AbortController();
// 存储控制器以便后续取消
this.pendingRequests.set(requestId, controller);
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
// 清理已完成的请求
this.pendingRequests.delete(requestId);
if (!response.ok) {
// 尝试解析错误信息
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorMessage;
} catch {
// 忽略 JSON 解析错误
}
throw new NetworkError(errorMessage, response.status);
}
// 处理空响应
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
// 区分中止错误和其他错误
if (error.name === 'AbortError') {
console.log('请求被取消:', endpoint);
throw new Error('请求已取消');
}
// 网络错误处理
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new NetworkError('网络连接失败,请检查网络设置', 0);
}
throw error;
} finally {
this.pendingRequests.delete(requestId);
}
}
cancelRequest(requestId) {
const controller = this.pendingRequests.get(requestId);
if (controller) {
controller.abort();
this.pendingRequests.delete(requestId);
}
}
cancelAllRequests() {
this.pendingRequests.forEach(controller => controller.abort());
this.pendingRequests.clear();
}
// 带重试的请求
async requestWithRetry(endpoint, options, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await this.request(endpoint, options);
} catch (error) {
lastError = error;
// 只有特定错误才重试
if (error instanceof NetworkError && error.isRetryable) {
if (attempt < maxRetries - 1) {
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
break;
}
}
throw lastError;
}
}
五、测试错误处理
javascript
// 使用 Jest 测试错误处理
describe('API Client Error Handling', () => {
test('处理网络错误', async () => {
fetchMock.mockReject(new Error('Network error'));
const client = new ApiClient('https://api.example.com');
await expect(client.request('/test'))
.rejects
.toThrow('网络连接失败');
});
test('处理 HTTP 错误状态', async () => {
fetchMock.mockResponse('', { status: 404 });
const client = new ApiClient('https://api.example.com');
await expect(client.request('/not-found'))
.rejects
.toThrow('HTTP 404');
});
test('请求取消功能', async () => {
const client = new ApiClient('https://api.example.com');
const requestId = 'test-request';
// 模拟长时间请求
fetchMock.mockResponse(() =>
new Promise(resolve =>
setTimeout(() => resolve({}), 1000)
)
);
const requestPromise = client.request('/slow', {}, requestId);
// 立即取消
client.cancelRequest(requestId);
await expect(requestPromise)
.rejects
.toThrow('请求已取消');
});
});
六、最佳实践总结
✅ 该做的:
-
总是处理 Promise 拒绝:即使只是记录日志
-
使用自定义错误类型:区分业务错误和系统错误
-
提供有意义的错误信息:包含上下文,便于调试
-
实施优雅降级:当非关键功能失败时继续运行
-
记录生产环境错误:但不要暴露敏感信息
❌ 不该做的:
-
不要忽略错误:空 catch 块是反模式
-
不要过度包装 try-catch:保持错误处理接近可能出错的代码
-
不要暴露堆栈给用户:但在开发环境中要保留
-
不要阻塞 UI:长时间的错误处理应该在后台进行
-
不要假设网络总是可用:实现离线处理
📊 错误处理决策树:
text
出现错误
├── 是网络错误?
│ ├── 是 → 可重试? → 是 → 实施指数退避重试
│ │ └── 否 → 显示网络错误提示
│ └── 否 → 继续
├── 是验证错误?
│ ├── 是 → 高亮相关字段
│ └── 否 → 继续
├── 是业务逻辑错误?
│ ├── 是 → 显示用户友好消息
│ └── 否 → 继续
└── 是未知错误?
├── 记录到监控服务
├── 显示通用错误消息
└── 保持应用可用状态
结语
良好的错误处理不仅是技术问题,更是产品思维。它关乎用户体验、系统稳定性和开发效率。通过实施这些最佳实践,你将能构建出更健壮、更可靠的前端应用。
记住:错误是不可避免的,但崩溃是可以避免的。优雅地处理错误,让你的应用在逆境中也能提供价值。
延伸阅读:
-
错误监控服务对比:Sentry vs Bugsnag vs Rollbar