async/await 到底要不要加 try catch?

📋 目录

🎯 概述

在使用 async/await 进行异步编程时,是否需要添加 try catch 是一个常见的困惑。本文将通过实例和最佳实践来解答这个问题。

简短答案: 取决于你的错误处理策略和应用场景。

核心原则: 在合适的地方,以合适的方式处理错误。

🔧 基础概念

async/await 的错误传播机制

// 没有 try catch 的情况
async function fetchData() {
  const response = await fetch('/api/data'); // 可能抛出错误
  const data = await response.json(); // 可能抛出错误
  return data;
}

// 调用时未处理错误
fetchData(); // ⚠️ 可能产生未捕获的 Promise rejection

Promise 错误处理的两种方式

// 方式1: try catch
async function method1() {
  try {
    const result = await someAsyncOperation();
    return result;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// 方式2: .catch()
async function method2() {
  return someAsyncOperation().catch((error) => {
    console.error('Error:', error);
    throw error;
  });
}

错误传播的本质

// async/await 本质上是 Promise 的语法糖
async function example() {
  // 这两种写法是等价的

  // 使用 async/await
  const data = await fetchData();

  // 等价的 Promise 写法
  return fetchData().then((data) => data);
}

✅ 什么时候需要 try catch

1. 需要错误恢复或转换时

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    // 错误恢复:返回默认值
    if (error.message.includes('404')) {
      return { id: userId, name: 'Unknown User' };
    }

    // 错误转换:包装为业务错误
    throw new Error(`Failed to fetch user ${userId}: ${error.message}`);
  }
}

2. 需要记录错误日志时

async function processOrder(order) {
  try {
    await validateOrder(order);
    await chargePayment(order);
    await updateInventory(order);
    await sendConfirmationEmail(order);

    return { success: true, orderId: order.id };
  } catch (error) {
    // 记录详细错误信息
    logger.error('Order processing failed', {
      orderId: order.id,
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
    });

    // 重新抛出或返回错误响应
    throw new Error(`Order ${order.id} processing failed`);
  }
}

3. 需要清理资源时

async function processFile(filePath) {
  let fileHandle = null;

  try {
    fileHandle = await fs.open(filePath, 'r');
    const data = await fileHandle.readFile();
    return await processData(data);
  } catch (error) {
    console.error('File processing error:', error);
    throw error;
  } finally {
    // 确保资源被清理
    if (fileHandle) {
      await fileHandle.close();
    }
  }
}

4. 顶层函数或入口点

// Express 路由处理器
app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id);
    res.json(user);
  } catch (error) {
    console.error('Route error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 主函数
async function main() {
  try {
    await initializeApp();
    await startServer();
  } catch (error) {
    console.error('Application startup failed:', error);
    process.exit(1);
  }
}

5. 需要部分错误恢复时

async function fetchMultipleData() {
  const results = [];

  for (const endpoint of endpoints) {
    try {
      const data = await fetch(endpoint);
      results.push(data);
    } catch (error) {
      // 单个失败不影响整体流程
      console.warn(`Failed to fetch ${endpoint}:`, error);
      results.push(null); // 或默认值
    }
  }

  return results;
}

❌ 什么时候不需要 try catch

1. 中间层函数,让错误向上传播

// ❌ 不必要的 try catch
async function getUserProfile(userId) {
  try {
    const user = await fetchUser(userId);
    const preferences = await fetchUserPreferences(userId);
    return { ...user, preferences };
  } catch (error) {
    throw error; // 只是重新抛出,没有实际处理
  }
}

// ✅ 简洁版本
async function getUserProfile(userId) {
  const user = await fetchUser(userId);
  const preferences = await fetchUserPreferences(userId);
  return { ...user, preferences };
}

2. 有统一错误处理机制时

// 有全局错误处理器
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // 记录日志、清理资源等
});

// Express 全局错误中间件
app.use((error, req, res, next) => {
  console.error(error);
  res.status(500).json({ error: 'Internal server error' });
});

// 这种情况下,业务逻辑可以更简洁
async function getProducts() {
  const products = await db.products.findAll();
  return products.map(formatProduct);
}

3. 使用 .catch() 更合适时

// 简单的错误处理用 .catch() 更简洁
async function deleteUser(userId) {
  return db.users.delete(userId).catch((error) => {
    if (error.code === 'NOT_FOUND') {
      return null; // 用户不存在,返回 null
    }
    throw error;
  });
}

4. 纯数据转换函数

// ❌ 过度使用 try catch
async function transformUserData(userData) {
  try {
    return {
      id: userData.id,
      name: userData.fullName,
      email: userData.emailAddress.toLowerCase(),
    };
  } catch (error) {
    throw error; // 无意义的包装
  }
}

// ✅ 让调用者处理错误
async function transformUserData(userData) {
  return {
    id: userData.id,
    name: userData.fullName,
    email: userData.emailAddress.toLowerCase(),
  };
}

🏆 最佳实践

1. 分层错误处理

// 数据层:专注于数据操作
class UserRepository {
  async findById(id) {
    // 不处理错误,让它向上传播
    return await db.users.findById(id);
  }
}

// 服务层:业务逻辑和错误转换
class UserService {
  constructor(userRepo) {
    this.userRepo = userRepo;
  }

  async getUser(id) {
    try {
      const user = await this.userRepo.findById(id);
      if (!user) {
        throw new Error('User not found');
      }
      return user;
    } catch (error) {
      // 转换为业务错误
      throw new UserServiceError(`Failed to get user ${id}`, error);
    }
  }
}

// 控制层:处理HTTP相关错误
class UserController {
  async getUser(req, res) {
    try {
      const user = await userService.getUser(req.params.id);
      res.json(user);
    } catch (error) {
      if (error instanceof UserServiceError) {
        res.status(404).json({ error: error.message });
      } else {
        res.status(500).json({ error: 'Internal server error' });
      }
    }
  }
}

2. 错误类型化

class APIError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = 'APIError';
    this.statusCode = statusCode;
  }
}

class ValidationError extends APIError {
  constructor(message) {
    super(message, 400);
    this.name = 'ValidationError';
  }
}

class NetworkError extends APIError {
  constructor(message) {
    super(message, 503);
    this.name = 'NetworkError';
  }
}

async function createUser(userData) {
  try {
    // 输入验证
    if (!userData.email) {
      throw new ValidationError('Email is required');
    }

    const user = await db.users.create(userData);
    return user;
  } catch (error) {
    // 数据库约束错误
    if (error.code === 'DUPLICATE_EMAIL') {
      throw new ValidationError('Email already exists');
    }

    // 网络相关错误
    if (error.code === 'ECONNREFUSED') {
      throw new NetworkError('Database connection failed');
    }

    // 其他错误向上传播
    throw error;
  }
}

3. 使用工具函数

// 错误处理工具函数
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// 使用工具函数简化路由
app.get(
  '/users/:id',
  asyncHandler(async (req, res) => {
    const user = await getUserById(req.params.id);
    res.json(user);
  })
);

// Promise 包装工具(Go 语言风格)
function to(promise) {
  return promise.then((data) => [null, data]).catch((error) => [error, null]);
}

// 使用示例
async function getUser(id) {
  const [error, user] = await to(fetchUser(id));

  if (error) {
    console.error('Failed to fetch user:', error);
    return null;
  }

  return user;
}

4. 重试机制

async function withRetry(fn, maxRetries = 3, delay = 1000) {
  let lastError;

  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (i === maxRetries) {
        throw error;
      }

      // 指数退避
      await new Promise((resolve) =>
        setTimeout(resolve, delay * Math.pow(2, i))
      );
    }
  }
}

// 使用示例
async function fetchWithRetry(url) {
  return withRetry(async () => {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  });
}

💡 实际示例

示例 1: API 调用封装

class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;

    try {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      // 网络错误或解析错误
      if (error instanceof TypeError) {
        throw new Error('Network error: Please check your connection');
      }

      // 重新抛出其他错误
      throw error;
    }
  }

  // 具体的 API 方法不需要 try catch
  async getUsers() {
    return this.request('/users');
  }

  async createUser(userData) {
    return this.request('/users', {
      method: 'POST',
      body: JSON.stringify(userData),
    });
  }
}

示例 2: 数据库操作

class DatabaseService {
  async transaction(operations) {
    const client = await this.pool.connect();

    try {
      await client.query('BEGIN');

      const results = [];
      for (const operation of operations) {
        const result = await operation(client);
        results.push(result);
      }

      await client.query('COMMIT');
      return results;
    } catch (error) {
      await client.query('ROLLBACK');
      console.error('Transaction failed:', error);
      throw new Error('Database transaction failed');
    } finally {
      client.release();
    }
  }

  // 具体的查询方法
  async findUser(id) {
    const query = 'SELECT * FROM users WHERE id = $1';
    const result = await this.pool.query(query, [id]);
    return result.rows[0];
  }
}

示例 3: 并发操作

async function processMultipleUsers(userIds) {
  const results = await Promise.allSettled(
    userIds.map(async (id) => {
      try {
        const user = await fetchUser(id);
        const processed = await processUser(user);
        return { id, success: true, data: processed };
      } catch (error) {
        console.error(`Failed to process user ${id}:`, error);
        return { id, success: false, error: error.message };
      }
    })
  );

  const successful = results
    .filter((result) => result.status === 'fulfilled' && result.value.success)
    .map((result) => result.value);

  const failed = results.filter(
    (result) =>
      result.status === 'rejected' ||
      (result.status === 'fulfilled' && !result.value.success)
  );

  return { successful, failed };
}

示例 4: 文件处理

class FileProcessor {
  async processFiles(filePaths) {
    const results = [];

    for (const filePath of filePaths) {
      try {
        const content = await this.readFile(filePath);
        const processed = await this.processContent(content);
        results.push({ filePath, success: true, data: processed });
      } catch (error) {
        console.error(`Failed to process ${filePath}:`, error);
        results.push({
          filePath,
          success: false,
          error: error.message,
        });
      }
    }

    return results;
  }

  async readFile(filePath) {
    // 让错误向上传播到 processFiles
    return await fs.readFile(filePath, 'utf8');
  }

  async processContent(content) {
    // 纯处理逻辑,不需要错误处理
    return content.split('\n').map((line) => line.trim());
  }
}

⚡ 性能考虑

try catch 的性能影响

// ❌ 过度使用 try catch 可能影响性能
async function processItems(items) {
  const results = [];

  for (const item of items) {
    try {
      // 对每个 item 都包装 try catch
      const result = await processItem(item);
      results.push(result);
    } catch (error) {
      console.error('Error processing item:', error);
    }
  }

  return results;
}

// ✅ 更好的方式:批量处理错误
async function processItems(items) {
  const promises = items.map(async (item) => {
    try {
      return await processItem(item);
    } catch (error) {
      console.error('Error processing item:', error);
      return null;
    }
  });

  return Promise.allSettled(promises);
}

避免深度嵌套

// ❌ 深度嵌套的 try catch
async function complexOperation() {
  try {
    const data = await fetchData();
    try {
      const processed = await processData(data);
      try {
        const result = await saveData(processed);
        return result;
      } catch (saveError) {
        console.error('Save error:', saveError);
      }
    } catch (processError) {
      console.error('Process error:', processError);
    }
  } catch (fetchError) {
    console.error('Fetch error:', fetchError);
  }
}

// ✅ 平铺的错误处理
async function complexOperation() {
  try {
    const data = await fetchData();
    const processed = await processData(data);
    const result = await saveData(processed);
    return result;
  } catch (error) {
    // 统一处理,根据错误类型进行不同处理
    if (error.type === 'FETCH_ERROR') {
      console.error('Fetch error:', error);
    } else if (error.type === 'PROCESS_ERROR') {
      console.error('Process error:', error);
    } else {
      console.error('Save error:', error);
    }
    throw error;
  }
}

🚫 常见误区

误区 1: 认为 try catch 是万能的

// ❌ 错误的认知
async function fetchData() {
  try {
    // 以为包装了 try catch 就万事大吉
    const response = await fetch('/api/data');
    return response.json(); // 如果 response 不是 JSON,这里会出错
  } catch (error) {
    console.error('Error:', error);
    return {}; // 返回空对象可能导致后续逻辑错误
  }
}

// ✅ 正确的处理
async function fetchData() {
  try {
    const response = await fetch('/api/data');

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const contentType = response.headers.get('content-type');
    if (!contentType || !contentType.includes('application/json')) {
      throw new Error('Response is not JSON');
    }

    return await response.json();
  } catch (error) {
    console.error('Error fetching data:', error);
    // 根据错误类型返回合适的默认值或重新抛出
    throw new Error(`Failed to fetch data: ${error.message}`);
  }
}

误区 2: 忽略 Promise.all 的错误处理

// ❌ 错误的并发处理
async function fetchMultipleData() {
  try {
    const results = await Promise.all([
      fetch('/api/users'),
      fetch('/api/posts'),
      fetch('/api/comments'),
    ]);
    return results;
  } catch (error) {
    // 任何一个失败,整个操作都失败
    console.error('Error:', error);
    return [];
  }
}

// ✅ 正确的并发处理
async function fetchMultipleData() {
  const results = await Promise.allSettled([
    fetch('/api/users').catch((error) => ({ error: error.message })),
    fetch('/api/posts').catch((error) => ({ error: error.message })),
    fetch('/api/comments').catch((error) => ({ error: error.message })),
  ]);

  return results.map((result) => {
    if (result.status === 'fulfilled') {
      return result.value;
    } else {
      return { error: result.reason.message };
    }
  });
}

误区 3: 过度捕获错误

// ❌ 过度捕获
async function getUserData(userId) {
  try {
    try {
      const user = await fetchUser(userId);
      try {
        const profile = await fetchUserProfile(userId);
        return { user, profile };
      } catch (profileError) {
        console.error('Profile error:', profileError);
        return { user, profile: null };
      }
    } catch (userError) {
      console.error('User error:', userError);
      throw userError;
    }
  } catch (error) {
    console.error('General error:', error);
    throw error;
  }
}

// ✅ 合理的错误处理
async function getUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const profile = await fetchUserProfile(userId).catch((error) => {
      console.warn('Profile fetch failed:', error);
      return null; // 用户资料是可选的
    });

    return { user, profile };
  } catch (error) {
    console.error('Failed to get user data:', error);
    throw new Error(`User ${userId} not found`);
  }
}

📋 总结

🎯 决策指南

需要 try catch 的情况:

  • ✅ 需要错误恢复或提供默认值
  • ✅ 需要转换错误类型或添加上下文
  • ✅ 需要记录错误日志
  • ✅ 需要清理资源(配合 finally)
  • ✅ 应用的入口点或边界函数
  • ✅ 需要向用户展示友好的错误信息
  • ✅ 需要部分错误恢复的场景

不需要 try catch 的情况:

  • ❌ 只是简单地重新抛出错误
  • ❌ 中间层函数,让错误自然向上传播
  • ❌ 已有统一的错误处理机制
  • ❌ 使用 .catch() 更简洁的场景
  • ❌ 纯数据转换函数

🔑 关键原则

  1. 就近处理原则:在最了解如何处理错误的地方进行处理
  2. 单一职责:每个 try catch 块应该有明确的处理目的
  3. 向上传播:不知道如何处理的错误应该向上传播
  4. 错误转换:将底层错误转换为业务层能理解的错误
  5. 用户友好:向用户展示有意义的错误信息
  6. 性能优先:避免不必要的 try catch 嵌套

📝 代码检查清单

  • 是否真的需要在这里处理错误?
  • 错误处理逻辑是否有意义?
  • 是否记录了足够的错误信息?
  • 是否正确清理了资源?
  • 错误信息对用户是否友好?
  • 是否考虑了错误恢复策略?
  • 是否避免了过度嵌套?
  • 是否考虑了性能影响?

🚀 进阶技巧

  1. 使用错误边界模式:在组件边界处理错误
  2. 实现重试机制:对于临时性错误进行重试
  3. 错误聚合:收集多个错误信息
  4. 监控和告警:生产环境错误监控
  5. 优雅降级:提供备选方案

📚 相关资源


记住:好的错误处理不是到处加 try catch,而是在正确的地方以正确的方式处理错误。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐