async/await 到底要不要加 try catch?
·
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()
更简洁的场景 - ❌ 纯数据转换函数
🔑 关键原则
- 就近处理原则:在最了解如何处理错误的地方进行处理
- 单一职责:每个 try catch 块应该有明确的处理目的
- 向上传播:不知道如何处理的错误应该向上传播
- 错误转换:将底层错误转换为业务层能理解的错误
- 用户友好:向用户展示有意义的错误信息
- 性能优先:避免不必要的 try catch 嵌套
📝 代码检查清单
- 是否真的需要在这里处理错误?
- 错误处理逻辑是否有意义?
- 是否记录了足够的错误信息?
- 是否正确清理了资源?
- 错误信息对用户是否友好?
- 是否考虑了错误恢复策略?
- 是否避免了过度嵌套?
- 是否考虑了性能影响?
🚀 进阶技巧
- 使用错误边界模式:在组件边界处理错误
- 实现重试机制:对于临时性错误进行重试
- 错误聚合:收集多个错误信息
- 监控和告警:生产环境错误监控
- 优雅降级:提供备选方案
📚 相关资源
记住:好的错误处理不是到处加 try catch,而是在正确的地方以正确的方式处理错误。 ✨
更多推荐
所有评论(0)