Promise 与 async/await 错误处理最佳实践指南

引言:为什么错误处理如此重要?

在异步编程中,错误处理常常被忽视,但它却是构建健壮应用的关键。想象一下:一个未处理的 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('请求已取消');
  });
});

六、最佳实践总结

✅ 该做的:

  1. 总是处理 Promise 拒绝:即使只是记录日志

  2. 使用自定义错误类型:区分业务错误和系统错误

  3. 提供有意义的错误信息:包含上下文,便于调试

  4. 实施优雅降级:当非关键功能失败时继续运行

  5. 记录生产环境错误:但不要暴露敏感信息

❌ 不该做的:

  1. 不要忽略错误:空 catch 块是反模式

  2. 不要过度包装 try-catch:保持错误处理接近可能出错的代码

  3. 不要暴露堆栈给用户:但在开发环境中要保留

  4. 不要阻塞 UI:长时间的错误处理应该在后台进行

  5. 不要假设网络总是可用:实现离线处理

📊 错误处理决策树:

text

复制代码
出现错误
    ├── 是网络错误?
    │   ├── 是 → 可重试? → 是 → 实施指数退避重试
    │   │               └── 否 → 显示网络错误提示
    │   └── 否 → 继续
    ├── 是验证错误?
    │   ├── 是 → 高亮相关字段
    │   └── 否 → 继续
    ├── 是业务逻辑错误?
    │   ├── 是 → 显示用户友好消息
    │   └── 否 → 继续
    └── 是未知错误?
        ├── 记录到监控服务
        ├── 显示通用错误消息
        └── 保持应用可用状态

结语

良好的错误处理不仅是技术问题,更是产品思维。它关乎用户体验、系统稳定性和开发效率。通过实施这些最佳实践,你将能构建出更健壮、更可靠的前端应用。

记住:错误是不可避免的,但崩溃是可以避免的。优雅地处理错误,让你的应用在逆境中也能提供价值。


延伸阅读

相关推荐
_OP_CHEN2 小时前
【Python基础】(三)Python 语法基础进阶:条件循环 + 实战案例,从入门到精通的核心跳板
开发语言·python·python入门·条件语句·循环语句·python基础语法
苹果电脑的鑫鑫2 小时前
.eslintrc.js这个文件作用
开发语言·javascript·ecmascript
ytttr8732 小时前
matlab进行利用遗传算法对天线阵列进行优化
开发语言·算法·matlab
无限进步_2 小时前
【C语言】队列(Queue)数据结构的实现与分析
c语言·开发语言·数据结构·c++·算法·链表·visual studio
特立独行的猫a2 小时前
Google C++ 编码规范核心要点总结 (2025精简版)
开发语言·c++·编码规范
vx_bisheyuange2 小时前
基于SpringBoot的便利店信息管理系统
前端·javascript·vue.js·毕业设计
晚烛2 小时前
智启工厂脉搏:基于 OpenHarmony + Flutter 的信创工业边缘智能平台构建实践
前端·javascript·flutter
Zsnoin能2 小时前
都快2026了,还有人不会国际化和暗黑主题适配吗,一篇文章彻底解决
前端·javascript
两个西柚呀2 小时前
es6和commonjs模块化规范的深入理解
前端·javascript·es6