引言:为什么错误处理如此重要?
在异步编程中,错误处理常常被忽视,但它却是构建健壮应用的关键。想象一下:一个未处理的 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
出现错误 ├── 是网络错误? │ ├── 是 → 可重试? → 是 → 实施指数退避重试 │ │ └── 否 → 显示网络错误提示 │ └── 否 → 继续 ├── 是验证错误? │ ├── 是 → 高亮相关字段 │ └── 否 → 继续 ├── 是业务逻辑错误? │ ├── 是 → 显示用户友好消息 │ └── 否 → 继续 └── 是未知错误? ├── 记录到监控服务 ├── 显示通用错误消息 └── 保持应用可用状态
结语
良好的错误处理不仅是技术问题,更是产品思维。它关乎用户体验、系统稳定性和开发效率。通过实施这些最佳实践,你将能构建出更健壮、更可靠的前端应用。
记住:错误是不可避免的,但崩溃是可以避免的。优雅地处理错误,让你的应用在逆境中也能提供价值。
延伸阅读:
MDN: Promise
Google: JavaScript 错误处理
[错误监控服务对比:Sentry vs Bugsnag vs Rollbar]