JavaScript async/await:异步函数的错误处理
1. 使用 try...catch 捕获标准错误
async 函数内部抛出的错误(无论是显式 throw 还是底层网络请求失败)都会导致返回的 Promise 变为 rejected 状态。最标准的处理方式是使用 try...catch 语句块。
定义一个可能抛出错误的异步函数。模拟一个网络请求失败的场景。
async function getUserData() {
// 模拟一个模拟 API 调用
const response = await fetch('https://api.example.com/user/123');
// fetch 只有在网络错误时才 reject,HTTP 404/500 依然视为 resolve
// 需要手动检查状态并抛出错误
if (!response.ok) {
throw new Error(`请求失败,状态码: ${response.status}`);
}
return await response.json();
}
```
**调用**该函数时,**使用** `try...catch` 包裹 `await` 操作。
```javascript
async function main() {
try {
console.log('开始获取数据...');
const data = await getUserData();
console.log('数据获取成功:', data);
} catch (error) {
// 无论 getUserData 是网络断开还是我们手动 throw 的错误,都会在这里被捕获
console.error('捕获到异常:', error.message);
// 在这里可以执行降级逻辑,比如显示默认数据
}
}
main();
```
---
## 2. 处理并发请求的“快速失败”与“全部完成”
在并发场景下,错误处理策略取决于你是否需要“只要有一个失败就全部取消”还是“无论成败都要拿到所有结果”。
### 场景 A:快速失败
**使用** `Promise.all` 时,只要其中一个 Promise 被拒绝,整个 `Promise.all` 就会立即拒绝。
**包裹** `await Promise.all` 并进行捕获。
```javascript
async function fetchAllCriticalData() {
try {
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
return { user, posts };
} catch (error) {
// 此时无法知道是 user 还是 posts 失败,只知道有东西挂了
console.error('关键数据请求失败,流程终止:', error.message);
// 适合:如果用户信息拿不到,文章列表也就没意义显示的情况
}
}
```
### 场景 B:全部完成
**切换**到 `Promise.allSettled`。这个方法不会因为单个失败而中断,它会等待所有 Promise 完成(无论成功还是失败)。
**遍历**返回的结果数组,**检查**每个结果的状态。
```javascript
async function fetchAllDataSafe() {
const results = await Promise.allSettled([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
const successfulResults = [];
const errors = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successfulResults.push(result.value);
} else {
console.error(`请求 ${index} 失败:`, result.reason.message);
errors.push(result.reason);
}
});
// 即使有部分失败,依然可以处理成功的数据
return { data: successfulResults, errors };
}
3. 消除嵌套地狱:使用高阶函数封装
当业务逻辑复杂,存在大量串行依赖的 await 操作时,层层嵌套的 try...catch 会导致代码缩进过深,难以维护。编写一个 to 工具函数,将错误处理转化为数组解构模式。
创建一个 to.js 文件,导出如下函数:
/**
* 将 Promise 的错误处理转为 [error, data] 形式
* @param {Promise} promise
* @returns {Promise<[Error|null, any]>}
*/
function to(promise) {
return promise
.then(data => [null, data])
.catch(err => [err, null]);
}
export default to;
重构业务代码,使用这个工具函数替代 try...catch。这种写法让代码始终保持“扁平”。
import to from './to.js';
async function processOrder() {
// 第一步:获取用户
const [userErr, user] = await to(fetch('/api/user').then(r => r.json()));
if (userErr) {
console.error('无法获取用户,终止流程');
return;
}
// 第二步:获取库存 (不再需要额外的 try 块包裹)
const [stockErr, stock] = await to(fetch(`/api/stock/${user.id}`).then(r => r.json()));
if (stockErr) {
console.error('库存查询失败,但用户信息已获取');
// 可以在这里执行降级操作,比如展示“库存暂时未知”
}
// 第三步:无论库存是否查到,都尝试记录日志
const [logErr] = await to(fetch('/api/log', {
method: 'POST',
body: JSON.stringify({ action: 'view' })
}).then(r => r.json()));
if (logErr) {
// 日志记录失败通常不影响主流程,仅打印警告
console.warn('日志记录失败');
}
console.log('流程执行完毕');
}
4. 顶层错误兜底
在 Node.js 环境或现代前端框架中,未捕获的 Promise 拒绝可能会导致程序崩溃或白屏。注册全局的未捕获 rejection 处理器作为最后一道防线。
添加以下代码到应用的入口文件(如 app.js 或 index.js)的最顶端:
// 浏览器环境
window.addEventListener('unhandledrejection', (event) => {
console.error('全局未捕获的 Promise 拒绝:', event.reason);
// 阻止默认的控制台报错输出(可选)
// event.preventDefault();
});
// Node.js 环境
process.on('unhandledRejection', (reason, promise) => {
console.error('全局未捕获的 Promise 拒绝:', reason);
// 建议在这里进行优雅关闭或上报监控系统
});
5. 方案对比与选择
下表总结了上述三种核心处理方式的适用场景,帮助你快速决策。
| 处理方式 | 适用场景 | 代码可读性 | 错误恢复难度 |
|---|---|---|---|
try...catch |
简单的线性逻辑,或者需要对不同错误执行完全不同分支时 | ⭐⭐⭐⭐⭐ | 简单,直接在 catch 块处理 |
to() 封装 |
长串行链路,或者希望保持代码扁平化,避免“回调金字塔” | ⭐⭐⭐⭐ (需适应解构写法) | 简单,通过 if (err) 判断 |
Promise.allSettled |
批量并行请求,且不能因为单个失败而丢失其他数据 | ⭐⭐⭐ | 中等,需遍历结果集逐个过滤 |

暂无评论,快来抢沙发吧!