摘要 :async/await 本质是 Promise 的语法糖,其错误处理也借鉴了同步代码的模式。try...catch 是 async/await 的「标配」------用块级语句包裹一段代码,捕获其中所有同步和异步(await)错误,一旦出错整个 try 块中断执行跳入 catch。.catch() 是 Promise 的原生链式方法------仅捕获调用它的那个 Promise 及前面链条中的错误,捕获后可返回新值让 async 函数「恢复」继续执行。核心区别在于流程控制模型:try...catch 是「中断」模型,.catch() 是「恢复」模型。选型原则:流程化、依赖性强的异步任务用 try...catch 统一处理;单个、非关键、可恢复的异步任务用 .catch() 独立处理。
一、async/await 与错误处理的关系
1.1 为什么需要专门的错误处理
async/await 让我们用同步代码的风格编写异步逻辑------代码从上到下顺序执行,不再有回调嵌套。因此它的错误处理也需要像同步代码一样直观:
js
// 同步代码的错误处理 ------ 你已经很熟悉了
try {
const data = JSON.parse(userInput); // 可能抛出 SyntaxError
console.log(data.name); // 如果上面抛错,这行不执行
} catch (error) {
console.error('解析失败:', error.message);
}
async/await 的 try...catch 就是把这套模式直接搬到了异步事件:
js
// 异步代码的错误处理 ------ 和同步写法一模一样!
async function fetchUser(userId) {
try {
const res = await fetch(`/api/users/${userId}`); // 可能网络错误
const data = await res.json(); // 可能 JSON 解析错误
console.log(data.name); // 任一 await 失败都进 catch
return data;
} catch (error) {
console.error('获取用户失败:', error.message);
return null; // 兜底返回值
}
}
1.2 await 的错误传播机制
当 await 后面的 Promise 被 reject (拒绝)时,await 表达式会 throw(抛出) 这个拒绝原因。这个抛出的错误可以被标准的 try...catch 语句捕获:
vbnet
┌─────────────────────────────────────────────────────┐
│ await Promise.reject(new Error("失败")) │
│ │ │
│ ▼ (自动 throw) │
│ ┌─────────────┐ │
│ │ try 块中断 │ ──→ 跳转到 catch(error) │
│ │ 后续代码不执行│ error = Error("失败") │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
关键理解 :await 不只是「等待 Promise 完成」,它还是一个潜在的 throw 点 。每个 await 都可能成为错误的来源。
二、try...catch:async/await 的标配
2.1 核心优势
| 优势 | 说明 |
|---|---|
| 语法统一 | 可以用与同步代码完全相同的方式捕获异步和同步错误 |
| 处理多个 await | 一个 try 块可以包含多个 await 表达式,任何一个失败都会被同一个 catch 捕获 |
| 包含 finally | 可以像同步代码一样使用 finally 块执行清理操作(关闭加载状态、释放资源),无论成功还是失败都会执行 |
2.2 基础示例
还原素材中的完整代码:
js
// 创建一个会失败的 Promise
const p = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve("妙码-成功");
reject(new Error("失败"));
}, 1000);
});
// 方式一:async/await + try...catch
async function invoke() {
try {
const res1 = await p; // 第一个 await: 抛出 Error("失败")
const res2 = await p; // 这行不会执行!(try 块已中断)
console.log(res1, res2); // 这行也不会执行
} catch (error) {
console.log(error.message); // 输出: "失败"
}
}
invoke();
执行过程分析:
javascript
时间线:
0ms invoke() 被调用, 进入 try 块
0ms await p → 暂停执行, 等待 p 的结果
1000ms p reject(Error("失败"))
→ await 将 Error 作为 throw 抛出
→ try 块立即中断!
→ const res2 = await p [跳过]
→ console.log(res1, res2) [跳过]
→ 进入 catch 块
→ 输出 "失败"
2.3 多个 await 共享一个 catch
这是 try...catch 最强大的特性------一系列相互依赖的异步操作共享同一个错误处理逻辑:
js
async function orderFlow(userId, itemId) {
let loading = true;
try {
// 步骤 1: 验证用户
const user = await verifyUser(userId);
// 步骤 2: 检查库存(依赖步骤 1 的结果)
const stock = await checkStock(itemId, user.level);
// 步骤 3: 扣减库存(依赖步骤 2)
const order = await createOrder(userId, itemId);
// 步骤 4: 支付(依赖步骤 3)
const payment = await processPayment(order.id);
// 全部成功
return { success: true, orderId: order.id };
} catch (error) {
// 任何一步失败都会到这里
// 错误处理逻辑集中管理,代码更清晰
console.error(`下单流程在某个环节失败:`, error.message);
// 根据错误类型做不同处理
if (error.code === 'INSUFFICIENT_STOCK') {
return { success: false, reason: '库存不足' };
}
if (error.code === 'PAYMENT_DECLINED') {
return { success: false, reason: '支付被拒' };
}
return { success: false, reason: '未知错误' };
} finally {
// 无论成功还是失败,都执行清理
loading = false;
hideLoadingSpinner();
}
}
设计思想:对于构成核心业务逻辑的一系列相互依赖的异步操作,应该将它们放在同一个 try...catch 块中。这使得错误处理逻辑集中,代码更清晰。如果其中任何一步失败,整个流程都应该终止。
三、.catch():Promise 的原生恢复机制
3.1 基本用法
.catch() 是 Promise 实例上的方法,通过链式调用附加:
js
// 方式二:Promise 链式 + .then().catch()
p.then(
(res) => {
console.log(res); // 成功时执行
},
(error) => console.log(error) // 失败时执行(第一个回调的"伴侣")
).finally(
(res) => console.log(res) // 无论成败都执行
);
// 另一种写法:独立的 .catch()
p.then(
(res) => {
console.log(res);
}
).catch(
(error) => console.log(error)
).finally(
() => {
console.log("完成");
}
);
3.2 .catch() 的「恢复」超能力
这是 .catch() 与 try...catch 最本质的区别:
js
async function invokeWithCatch() {
// .catch() 捕获错误后可以返回一个新值
// 整个 async 函数从这个错误中"恢复"过来,继续向下执行!
const result = await p.catch((error) => {
console.log('捕获到错误:', error.message); // 输出: "失败"
return '默认值'; // ← 关键:返回一个新值作为"恢复值"
});
console.log('函数继续执行, result =', result); // 输出: "默认值"
console.log('后续代码正常运行!');
return result; // 返回 "默认值",不会 reject
}
invokeWithCatch();
对比两种模式:
js
// 模式 A: try...catch ------ 中断模式
async function patternA() {
try {
const res = await p; // 失败 → throw
console.log('这行不执行'); // 被跳过
} catch (e) {
console.log(e.message);
}
console.log('这里正常执行'); // catch 之后继续
}
// 模式 B: .catch() ------ 恢复模式
async function patternB() {
const res = await p.catch(() => 'fallback'); // 失败 → 恢复为 'fallback'
console.log('这行照常执行, res =', res); // res = 'fallback'
console.log('流程没有中断!');
}
四、try...catch vs .catch() 全面对比
4.1 特性对比表(还原素材并扩展)
| 维度 | try...catch | .catch() (Promise 方法) |
|---|---|---|
| 语法结构 | 块级语句,包裹一段代码 | Promise 实例方法,通过链式调用附加 |
| 作用范围 | 捕获 try 块内所有的同步和异步(await)错误 | 仅捕获调用它的那个 Promise 及其前面链条中的错误 |
| 流程控制 | 中断模型:错误发生时立即跳出 try 块,执行 catch 块。后续 await 不执行 | 恢复模型:捕获错误后可返回新值,让 async 函数继续向下执行,不会中断 |
| 代码风格 | 命令式、同步风格 | 函数式、链式风格 |
| finally 支持 | ✅ try {} catch(e) {} finally {} |
✅ .then().catch().finally() |
| 多错误统一处理 | ✅ 一个 catch 处理所有 await | ❌ 每个 Promise 需要各自 .catch() |
| 精确错误定位 | ❌ 不知道哪个 await 出错(需额外判断) | ✅ 精确知道哪个 Promise 失败 |
| 混合同步/异步 | ✅ 同时捕获同步错误(如 JSON.parse)和异步错误 | ❌ 只能捕获 Promise 错误,同步错误需额外处理 |
| 嵌套复杂度 | 深层嵌套时 try 层级变多 | 链式扁平,但过长时可读性下降 |
4.2 流程控制的核心区别(重点!)
这是面试最高频考点:
dart
┌── try...catch: 中断模型 ──────────────────────────────┐
│ │
│ try { │
│ await step1(); ✅ │
│ await step2(); ❌ throw! │
│ await step3(); ⛔ 跳过! (try 块已中断) │
│ } │
│ catch(e) { ... } ← 在此处理错误 │
│ // catch 之后的代码正常执行 │
│ │
└────────────────────────────────────────────────────────┘
┌── .catch(): 恢复模型 ─────────────────────────────────┐
│ │
│ const r1 = await step1().catch(e => fallback1); ✅ │
│ const r2 = await step2().catch(e => fallback2); ✅ │
│ const r3 = await step3().catch(e => fallback3); ✅ │
│ // 即使 step2 失败, step3 照常执行! │
│ // 每个步骤独立容灾, 互不影响 │
│ │
└────────────────────────────────────────────────────────┘
五、实战场景与选型指南
5.1 场景一:核心业务流程 --- 首选 try...catch
当一系列异步操作相互依赖、构成完整事务时,用 try...catch:
js
// 电商下单完整流程:任一步失败则整体回滚
async function placeOrder(orderData) {
const transactionId = generateId();
try {
// 1. 锁定库存
await lockInventory(orderData.items);
// 2. 创建订单
const order = await createOrderInDB(orderData, transactionId);
// 3. 扣款
await chargePayment(orderData.payment, order.total);
// 4. 发送确认通知
await sendConfirmationEmail(orderData.email, order.id);
// 全部成功 → 提交事务
await commitTransaction(transactionId);
return { status: 'success', order };
} catch (error) {
// 任何步骤失败 → 回滚整个事务
await rollbackTransaction(transactionId);
// 记录错误日志
await logError({
transactionId,
step: 'placeOrder',
error: error.message,
timestamp: Date.now()
});
// 通知用户
showErrorMessage('下单失败,请稍后重试');
return { status: 'failed', reason: error.message };
}
}
为什么不用 .catch()? 因为步骤 2~4 都依赖前一步的结果。如果步骤 2 失败了,步骤 3 和 4 根本不应该执行。try...catch 的「中断」语义完美匹配这种需求。
5.2 场景二:非关键独立任务 --- 用 .catch()
当一个异步操作非关键、或其失败不应中断整个函数流程 时,用行内的 .catch():
js
async function loadHomePage(userId) {
// 关键数据:必须成功,用 try...catch
let userData;
try {
userData = await fetchUserData(userId);
} catch (e) {
redirectToLogin();
return; // 关键数据失败,直接终止
}
// 主要内容:必须成功
const articles = await fetchArticles().catch(() => []);
if (!articles.length) {
showEmptyState();
}
// ====== 下面是非关键内容 ======
// 广告数据:失败了没关系,页面照样展示
const ads = await fetchAds().catch(() => []);
renderAds(ads);
// 推荐内容:失败了显示默认推荐
const recommendations = await fetchRecommendations(userId)
.catch(() => getDefaultRecommendations());
renderRecommendations(recommendations);
// 用户行为埋点:完全不应影响用户体验
trackPageView('/home').catch(() => {}); // 静默失败
// A/B 测试配置:取不到就用默认值
const abConfig = await fetchABConfig()
.catch(() => ({ showBanner: true, theme: 'light' }));
applyTheme(abConfig.theme);
return { userData, articles, ads, recommendations };
}
为什么这里用 .catch()? 广告加载失败不应该导致整个白屏;埋点失败更不能影响用户体验。每个非关键操作独立容灾,互不影响。
5.3 场景三:两者混用的最佳实践
实际项目中,最优雅的方式是组合使用:
js
class UserService {
async getUserProfile(userId) {
// 外层:核心流程用 try...catch 保证原子性
try {
// 并行请求无依赖的数据
const [basicInfo, permissions, preferences] = await Promise.all([
this.fetchBasicInfo(userId),
this.fetchPermissions(userId),
// 非关键数据:单独 .catch() 提供默认值
this.fetchPreferences(userId).catch(() => ({
theme: 'light',
language: 'zh-CN',
notifications: true
}))
]);
// 有依赖的后续操作继续在 try 内
const enrichedData = await this.enrichUserData(basicInfo, permissions);
return { ...enrichedData, preferences };
} catch (error) {
// 统一的错误处理 + 上报
reportError(error, { userId, context: 'getUserProfile' });
// 根据错误类型提供不同的降级策略
if (error.status === 401) {
throw new AuthError('登录已过期'); // 向上抛出让调用方处理
}
if (error.status === 403) {
return getGuestProfile(); // 权限不足 → 降级为游客模式
}
// 其他未知错误 → 返回最小可用数据
return getMinimalProfile();
}
}
}
六、高级技巧与常见陷阱
6.1 技巧一:带分类的 catch 处理
js
class AppError extends Error {
constructor(message, code, statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
}
class NetworkError extends AppError {
constructor(originalError) {
super(originalError.message, 'NETWORK_ERROR', 503);
this.originalError = originalError;
}
}
class ValidationError extends AppError {
constructor(fields) {
super('参数校验失败', 'VALIDATION_ERROR', 400);
this.fields = fields;
}
}
// 使用
async function submitForm(formData) {
try {
validate(formData); // 可能抛 ValidationError
const result = await api.submit(formData); // 可能抛 NetworkError
return result;
} catch (error) {
if (error instanceof NetworkError) {
showRetryButton(); // 网络错误 → 提示重试
} else if (error instanceof ValidationError) {
highlightFields(error.fields); // 校验错误 → 高亮字段
} else {
showErrorDialog(error.message); // 其他错误 → 通用提示
}
throw error; // 可以选择继续向上抛出
}
}
6.2 技巧二:async 函数内的同步错误也会被 catch
js
async function demo() {
try {
const data = JSON.parse('{ invalid json'); // 同步 SyntaxError!
const res = await fetch('/api/data'); // 这行不执行
} catch (error) {
// 同时捕获了同步错误和异步错误!
console.log(error instanceof SyntaxError); // true
}
}
这是 try...catch 相比 .catch() 的独特优势------同时覆盖同步和异步两类错误。
6.3 陷阱一:未捕获的 Promise rejection
js
// 危险! 如果 forgetToAwait() 内部的 Promise 失败,
// 错误不会被外层 try...catch 捕获!
async function dangerous() {
try {
forgetToAwait(); // 没有 await! Promise 在"后台"运行
console.log('继续执行...');
} catch (e) {
// 永远不会到达这里!
}
}
function forgetToAwait() {
return Promise.reject(new Error('未捕获'));
}
// 正确写法
async function safe() {
try {
await forgetToAwait(); // 加上 await!
} catch (e) {
console.log('正确捕获:', e.message); // "未捕获"
}
}
6.4 陷阱二:forEach 中的 async/await 无效
js
// 坑! forEach 不支持 async callback
const ids = [1, 2, 3];
ids.forEach(async (id) => {
try {
const data = await fetchData(id);
console.log(data);
} catch (e) {
console.error(e); // 虽然 catch 能工作...
}
});
// 但外层无法等待所有 forEach 完成!
// 正确: for...of 或 for 循环
for (const id of ids) {
try {
const data = await fetchData(id);
console.log(data);
} catch (e) {
console.error(e);
}
}
// 或: map + Promise.all
await Promise.all(ids.map(async (id) => {
try {
return await fetchData(id);
} catch (e) {
return null; // 单个失败不影响其他
}
}));
6.5 陷阱三:catch 中再次 throw
js
async function rethrowDemo() {
try {
await riskyOperation();
} catch (error) {
console.log('第一层捕获:', error.message);
// 选择 1: 处理后不再抛出 → 函数正常返回
// return defaultValue;
// 选择 2: 包装后重新抛出 → 让上层处理
throw new Error(`操作失败: ${error.message}`, { cause: error });
// 选择 3: 直接原样抛出
// throw error;
}
}
// 调用方
try {
await rethrowDemo();
} catch (e) {
console.log('第二层捕获:', e.message); // "操作失败: ..."
console.log('原始错误:', e.cause?.message); // ES2022 Error.cause
}
6.6 技巧四:高阶封装 --- withRetry
js
// 自动重试装饰器
async function withRetry(fn, options = {}) {
const {
retries = 3,
delay = 1000,
backoff = 2, // 指数退避倍数
shouldRetry = (e) => e.statusCode >= 500, // 仅服务端错误重试
} = options;
let lastError;
for (let i = 0; i <= retries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i === retries || !shouldRetry(error)) {
throw error; // 重试耗尽或不可重试 → 最终抛出
}
const waitTime = delay * Math.pow(backoff, i);
console.log(`第 ${i + 1} 次重试, ${waitTime}ms 后执行...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
throw lastError;
}
// 使用
const data = await withRetry(
() => fetch('/api/expensive-data').then(r => r.json()),
{ retries: 3, delay: 1000 }
);
七、完整流程串联
下面用一个综合示例走通全部知识点:
js
/**
* 场景:电商首页数据加载
* 要求:
* - 用户信息是关键的,失败则跳转登录
* - 商品列表是主要的,失败显示空状态
* - 广告/推荐/埋点是非关键的,失败静默降级
*/
async function loadEcommerceHome(userId) {
// ══════════════════════════════════════
// 第一层:关键数据 --- try...catch 统一处理
// ══════════════════════════════════════
let user;
try {
// 同步校验也可能出错(try...catch 同时捕获)
if (!userId || typeof userId !== 'string') {
throw new ValidationError(['userId'], '无效的用户 ID');
}
// 核心 API:必须成功
user = await withRetry(
() => fetch(`/api/users/${userId}`).then(r => {
if (!r.ok) throw new AppError(r.statusText, 'API_ERROR', r.status);
return r.json();
}),
{ retries: 2 }
);
} catch (error) {
// 分类处理
if (error instanceof ValidationError) {
showToast('参数错误,请刷新页面重试');
} else if (error?.statusCode === 401) {
redirectToLogin(); // 未授权 → 跳转登录
return; // 终止整个函数
} else {
showToast('网络异常,请检查连接');
sendErrorReport(error); // 错误上报
}
user = getGuestUser(); // 降级为游客
}
// ══════════════════════════════════════
// 第二层:主要数据 --- try...catch + 降级
// ══════════════════════════════════════
let products = [];
try {
products = await fetchProducts({ category: 'featured' });
} catch (e) {
products = []; // 空列表降级
showEmptyProductsPlaceholder();
}
// ══════════════════════════════════════
// 第三层:非关键数据 --- 各自 .catch() 独立容灾
// ══════════════════════════════════════
// 广告:失败用空数组
const banners = await fetchBanners()
.catch(() => []);
renderBanners(banners);
// 个性化推荐:失败用热门商品替代
const recommendations = await fetchRecommendations(user.tags)
.catch(() => getTrendingProducts());
renderRecommendations(recommendations);
// 购物车数量:失败显示 0
const cartCount = await fetchCartCount(userId)
.catch(() => 0);
updateCartBadge(cartCount);
// 行为埋点:纯副作用,失败完全忽略
trackPageView('home', { userId })
.catch(() => {}); // 静默吞掉
// ══════════════════════════════════════
// 清理(无论成败都执行)
// ══════════════════════════════════════
hideGlobalLoader();
return { user, products, banners, recommendations, cartCount };
}
// 调用
loadEcommerceHome('user_123')
.then(data => console.log('首页加载完成', data))
.catch(err => console.error('致命错误(未被内部捕获)', err));
执行流程图:
ini
loadEcommerceHome('user_123')
│
├─[try] 校验 userId + fetch 用户信息
│ ├─ 成功 → user = 数据
│ └─ 失败 → catch → 分类处理 / 降级 guest / 或 return 终止
│
├─[try] fetch 商品列表
│ ├─ 成功 → products = 数据
│ └─ 失败 → catch → products = [] + 显示空状态占位
│
├─[.catch()] fetch 广告 → 失败则 banners = []
├─[.catch()] fetch 推荐 → 失败则用热门替代
├─[.catch()] fetch 购物车 → 失败则 cartCount = 0
├─[.catch()] trackPageView → 失败则静默忽略
│
├─[finally] hideGlobalLoader()
│
└─ return { user, products, banners, recommendations, cartCount }
八、速查表与记忆口诀
8.1 选型决策树
dart
这个异步操作失败后需要怎样?
│
├── 整个流程都应该停止?
│ └── ✅ 用 try...catch 包裹整段逻辑
│
├── 可以用默认值/降级方案代替?
│ └── ✅ 用行内 .catch(() => defaultValue)
│
├── 需要知道具体是哪步失败?
│ └── ✅ 每个 await 单独 .catch() 或分多个 try 块
│
└── 需要做清理操作(无论成败)?
└── ✅ try 的 finally 或 .finally()
8.2 记忆口诀
try 包裹依赖流,一错全停进 catch
catch 链式能恢复,返回新值继续走
关键数据用 try,非关键用 catch
同步异步都能抓,finally 清理忘不了
8.3 一句话总结
- try...catch = async/await 的「标配」,用于处理流程化、依赖性强 的异步任务,提供集中式的错误处理
- .catch() = Promise 的原生方法 ,在 async/await 中可用于处理单个、非关键、可恢复 的异步任务,实现更精细的流程控制
九、面试高频 Q&A
Q1: try...catch 和 .catch() 本质区别是什么?
A: 流程控制模型不同。try...catch 是「中断」模型------一旦出错,整个 try 块剩余代码全部跳过。.catch() 是「恢复」模型------捕获错误后可返回新值,让 async 函数继续执行。
Q2: 什么时候该用 try...catch?什么时候用 .catch()?
A: 判断标准------如果失败后后续步骤不应该执行 (如依赖上一步结果的业务流程),用 try...catch;如果失败后可以用默认值替代且不影响其他操作(如广告/埋点/推荐),用 .catch()。
Q3: try...catch 能捕获同步错误吗?
A: 能!这是它相比 .catch() 的独特优势。JSON.parse 抛出的 SyntaxError、手动 throw 的错误、甚至 TypeError(如 null.property),只要在 try 块内发生,都会被 catch 捕获。
Q4: async 函数中忘记 await 一个 reject 的 Promise 会怎样?
A: 它变成一个「未捕获的 Promise rejection」,不会被外层的 try...catch 捕获。在浏览器中会在控制台警告,在 Node.js 中可能导致进程退出(取决于版本)。解决方式:始终 await 或 .catch() 每个 Promise。
Q5: .catch() 里面再 throw 会怎样?
A: 错误会继续向上冒泡。可以在 catch 中做「错误包装」(如添加上下文信息后重新 throw),让上层调用方处理。ES2022 支持 new Error(msg, { cause: originalError }) 保持错误链。