async/await 错误处理:try-catch 不是唯一答案
async/await 让异步代码更优雅,但错误处理却是个坑。本文教你多种实用的错误处理模式。
10 分钟阅读
小明
从一个线上事故说起
凌晨 2 点,手机响了。
"小明,线上挂了,用户反馈页面白屏!"
我迷迷糊糊打开电脑,查看错误日志:
Uncaught (in promise) TypeError: Cannot read property 'name' of undefined
问题出在一个 async 函数里,有个接口返回了 null,但代码直接用了 response.data.name。
没有 try-catch,没有错误处理,一个未捕获的异常就这样把整个页面搞崩了。
这篇文章,我们就来彻底搞定 async/await 的错误处理。
基础:try-catch 的正确用法
最简单的 try-catch
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data;
} catch (error) {
console.error('获取用户失败:', error);
return null;
}
}
看起来很简单对吧?但这里有个问题:try 块里包含了太多代码。
问题:try 块里到底该包什么?
看这段代码:
async function processUser(id) {
try {
const user = await fetchUser(id);
const orders = await fetchOrders(user.id);
const payments = await fetchPayments(orders);
await sendEmail(user.email, payments);
await updateDatabase(user.id, { lastLogin: new Date() });
return { success: true };
} catch (error) {
// 这里的 error 可能来自任何一步
// 你怎么知道是哪一步出了问题?
console.error('处理失败:', error);
return { success: false };
}
}
问题很明显:
- 不知道错误来自哪一步
- 不同的错误需要不同的处理方式
- 部分步骤失败时,可能需要回滚之前的操作
更好的做法:精确捕获
async function processUser(id) {
// 获取用户
let user;
try {
user = await fetchUser(id);
} catch (error) {
console.error('获取用户失败:', error);
return { success: false, step: 'fetchUser' };
}
// 获取订单
let orders;
try {
orders = await fetchOrders(user.id);
} catch (error) {
console.error('获取订单失败:', error);
return { success: false, step: 'fetchOrders' };
}
// ... 后续步骤类似
return { success: true };
}
但这样写太啰嗦了!有没有更优雅的方式?
模式一:await-to-js 风格
灵感来自 Go 语言的错误处理风格:
// Go 语言
data, err := fetchData()
if err != nil {
// 处理错误
}
我们可以封装一个工具函数:
/**
* 包装 Promise,返回 [error, data] 格式
* @param {Promise} promise
* @returns {Promise<[Error | null, T | null]>}
*/
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
使用方式:
async function processUser(id) {
const [userError, user] = await to(fetchUser(id));
if (userError) {
console.error('获取用户失败:', userError);
return { success: false, step: 'fetchUser' };
}
const [ordersError, orders] = await to(fetchOrders(user.id));
if (ordersError) {
console.error('获取订单失败:', ordersError);
return { success: false, step: 'fetchOrders' };
}
const [paymentsError, payments] = await to(fetchPayments(orders));
if (paymentsError) {
console.error('获取支付记录失败:', paymentsError);
return { success: false, step: 'fetchPayments' };
}
return { success: true, data: { user, orders, payments } };
}
优点:
- 每一步的错误都能精确捕获
- 代码扁平,不需要嵌套的 try-catch
- 可以在错误发生后继续执行其他操作
缺点:
- 每一步都要判断错误,略显繁琐
模式二:Promise 的 catch 方法
别忘了,async 函数返回的是 Promise,可以用 .catch() 来处理错误:
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// 调用时处理错误
const user = await fetchUser(123).catch(error => {
console.error('获取用户失败:', error);
return null; // 返回默认值
});
if (user) {
console.log('用户名:', user.name);
}
这种方式特别适合需要提供默认值的场景:
async function getConfig() {
const config = await fetchConfig().catch(() => ({
theme: 'light',
language: 'zh-CN',
}));
return config;
}
模式三:统一错误处理层
在实际项目中,我推荐在 API 层统一处理错误:
// api.js - 封装统一的请求函数
class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
async function request(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
const data = await response.json();
if (!response.ok) {
throw new ApiError(
data.message || '请求失败',
response.status,
data
);
}
return data;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
// 网络错误
if (error.name === 'TypeError') {
throw new ApiError('网络连接失败,请检查网络', 0, null);
}
throw new ApiError('未知错误', -1, error);
}
}
// 业务 API
export const userApi = {
getUser: (id) => request(`/api/users/${id}`),
updateUser: (id, data) => request(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
};
在组件中使用:
async function loadUser() {
try {
const user = await userApi.getUser(123);
setState({ user, loading: false });
} catch (error) {
if (error.status === 404) {
setState({ error: '用户不存在', loading: false });
} else if (error.status === 401) {
router.push('/login');
} else {
setState({ error: error.message, loading: false });
}
}
}
模式四:全局错误捕获
有些错误你可能忘记处理了,设置全局捕获作为最后防线:
// 捕获未处理的 Promise rejection
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 错误:', event.reason);
// 上报到监控系统
reportError({
type: 'unhandledrejection',
error: event.reason,
url: window.location.href,
});
// 可以选择阻止默认行为(在控制台显示错误)
// event.preventDefault();
});
注意:这只是兜底方案,不能替代正常的错误处理!
模式五:重试机制
有些错误是临时的,比如网络抖动。这时候可以实现自动重试:
async function retry(fn, options = {}) {
const {
maxRetries = 3,
delay = 1000,
backoff = 2, // 指数退避系数
shouldRetry = () => true,
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
break;
}
if (!shouldRetry(error)) {
break;
}
const waitTime = delay * Math.pow(backoff, attempt);
console.log(`重试 ${attempt + 1}/${maxRetries},等待 ${waitTime}ms`);
await sleep(waitTime);
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用
const user = await retry(
() => fetchUser(123),
{
maxRetries: 3,
delay: 1000,
shouldRetry: error => error.status >= 500, // 只重试服务端错误
}
);
模式六:组合错误处理
在复杂场景下,可能需要组合多种模式:
// 工具函数
const to = (promise) => promise.then(d => [null, d]).catch(e => [e, null]);
// API 层:统一错误处理 + 重试
async function robustFetch(url, options) {
return retry(
async () => {
const response = await fetch(url, options);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
},
{
maxRetries: 3,
shouldRetry: (error) => error.status >= 500,
}
);
}
// 业务层:精确捕获
async function checkout(cartId) {
// 1. 验证购物车
const [cartError, cart] = await to(validateCart(cartId));
if (cartError) {
return { success: false, error: '购物车验证失败' };
}
// 2. 创建订单
const [orderError, order] = await to(createOrder(cart));
if (orderError) {
return { success: false, error: '订单创建失败' };
}
// 3. 处理支付(失败需要取消订单)
const [paymentError, payment] = await to(processPayment(order));
if (paymentError) {
await cancelOrder(order.id); // 回滚
return { success: false, error: '支付失败' };
}
return { success: true, orderId: order.id };
}
React/Vue 中的最佳实践
React 中使用 Error Boundary
// ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>出错了:{this.state.error.message}</div>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary>
<UserProfile userId={123} />
</ErrorBoundary>
Vue 中使用 errorCaptured
<script setup>
import { onErrorCaptured, ref } from 'vue';
const error = ref(null);
onErrorCaptured((err, instance, info) => {
error.value = err;
reportError(err, info);
return false; // 阻止错误继续向上传播
});
</script>
<template>
<div v-if="error">出错了:{{ error.message }}</div>
<slot v-else />
</template>
总结:我的错误处理策略
经过多年的实践,我总结出这套策略:
- API 层:统一封装,统一错误格式
- 业务层:使用
to()函数精确捕获 - 组件层:使用 Error Boundary 兜底
- 全局层:监听
unhandledrejection - 临时错误:实现重试机制
┌─────────────────────────────────────────────┐
│ 全局错误监听 (unhandledrejection) │
│ ┌─────────────────────────────────────────┐│
│ │ Error Boundary ││
│ │ ┌─────────────────────────────────────┐││
│ │ │ 业务层:to() 精确捕获 │││
│ │ │ ┌─────────────────────────────────┐│││
│ │ │ │ API 层:统一封装 + 重试 ││││
│ │ │ └─────────────────────────────────┘│││
│ │ └─────────────────────────────────────┘││
│ └─────────────────────────────────────────┘│
└─────────────────────────────────────────────┘
记住:好的错误处理应该是分层的、可预测的、有兜底的。
下一篇,我们来聊聊"程序员的春节"——如何在假期保持学习状态又不累垮自己。