想象你开了一家小有名气的餐厅。顾客点完菜,你写一张订单递给后厨,然后转身继续招呼其他客人——这就是异步(async)。后厨做好菜喊一声“好了”,你才停下手里的事去端菜送客——这就是等待(await)。一切看起来顺畅高效,但后厨总有意外:食材缺货、灶台罢工、厨师手滑摔了盘子。如果这些意外没有预案,轻则一道菜报废,重则整个餐厅乱成一锅粥。async/await 的错误处理艺术,就是教你如何在后厨出问题时稳住全场:知道哪些意外该由谁接住(捕获边界)、坏消息怎么传达(await 的异常传播)、多道菜同时出问题时如何止损(并行处理与隔离),以及如何在最坏情况下给出体面的退路(重试与降级)。掌握这套方法论,你写的代码才能从“能跑”,变成“能在生产环境里扛住风雨”。

岗位关联

对 AI 从业者而言,async/await 错误处理是生产级 AI 应用(LLM API 调用、Agent 任务调度、数据采集)稳定运行的基石。不会妥善处理异步错误,你的服务会在请求超时、工具调用失败时直接崩溃,更谈不上重试与降级。

目录
  1. 可视化演示
  2. 捕获范围边界
  3. await异常捕获
  4. 并行await处理
  5. 统一错误处理
  6. 重试机制
  7. 优雅降级
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示try/catch捕获范围、Promise.all与allSettled并行错误策略、指数退避重试机制三种核心错误处理模式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

try/catch捕获范围 try块包裹await调用,reject自动抛出 错误沿调用栈传播至catch块 成功路径跳过catch继续执行 await reject ⟹ throw → catch await的reject等价于throw,被最近try/catch捕获

捕获范围边界

餐厅里有一条不成文的规矩:服务员能管的只有自己负责的区域。你在 A 区服务,B 区出了乱子,你跑过去也插不上手。try/catch 也是一样——它是一块“责任区”,只在代码执行流进入 try 块时生效。一旦执行流跳出这块区域,比如进入 setTimeout 回调、事件监听器或者另一个没有被 await 的 Promise,try/catch 就鞭长莫及了。很多新手以为只要写了 try/catch 就万事大吉,结果错误从“后门”溜进来,程序照样崩溃。

在 async/await 的世界里,try/catch 的捕获范围遵循一个清晰边界:它只能捕获 await 表达式的拒绝、async 函数体内同步抛出的异常,以及被 await 的 Promise 的 reject。对于 setTimeout 这类“把代码交给别人执行”的场景,执行流已经离开了 try 的管辖,错误发生时 try/catch 早已“下班”。

来看一个典型例子:

setTimeout 回调里的 throw 发生在 try/catch 之外,无法被捕获,最终会变成 uncaught exception。而 await fetch 的 reject 会被 catch 正常接收。这就是边界:try/catch 不是全局捕虫网,它只在“执行流经过自己”的那一刻有管辖权。

回到餐厅场景:try/catch 就像负责 1 号桌的服务员。1 号桌客人投诉(reject),他立刻接住处理;但如果客人已经走到门口才摔倒(setTimeout 回调里的错误),服务员已经不在那个位置了,自然管不着。理解这个边界,你才能正确地分配“服务员”,知道哪些区域需要额外派人(比如在回调内部补 .catch 或 finally 兜底)。

try{await f(x)}⇒{v成功返回e失败交给 catch\text{try}\{\text{await } f(x)\} \Rightarrow \begin{cases} v & \text{成功返回} \\ e & \text{失败交给 catch} \end{cases}
try 块就像餐厅的服务区,只有在本区域内发生的错误(await 的 reject)才会被 catch 接住。
  1. try 开始执行进入 try 块,await 暂停当前 async 函数,等待 f(x) 的结果
    try{await f(x)}\text{try}\{\text{await } f(x)\}
  2. 成功返回 vf(x) 正常 resolve,返回值 v 赋给等号左侧,继续执行后续代码,catch 被跳过
    v←await f(x)v \leftarrow \text{await } f(x)
  3. 失败抛异常 ef(x) 返回 rejected 状态,原因 e 被当作异常抛出,传给最近的 catch
    e⇒catch(e)e \Rightarrow \text{catch}(e)
js8 行
async function load() {  try {    setTimeout(() => { throw new Error('boom'); }, 100);    await fetch('/api/data'); // 这里能捕获  } catch (e) {    console.log('捕获:', e);  }}

await异常捕获

顾客点了一份炒饭,后厨忙活了半天,跑出来说“米没了”。这时候服务员不能假装没事,得把坏消息传达给顾客。在 async/await 的世界里,await 就是那个服务员:它负责把后厨的“坏消息”(Promise.reject)翻译成顾客能懂的“退单通知”(throw)。这个翻译动作是自动的——只要 Promise 拒绝,await 就会把拒绝的原因当作异常抛出来。

具体来说,await promise 表达式的行为是:如果 promise resolve,表达式返回其值;如果 promise reject,表达式抛出其 reason。这就是 await 最神奇的“对称性”——它把异步的两态结果(resolve/reject)翻译成了同步的两态结构(正常返回/异常抛出)。有了这层翻译,你就可以用熟悉的 try/catch 处理异步错误,代码读起来和同步代码一样自然。

这个机制在 async 函数内部也成立:async 函数体内同步抛出的 throw,会变成函数返回的 rejected Promise。所以 async function f() { throw new Error('x'); } 和 async function f() { return Promise.reject(new Error('x')); } 在外部看来完全等价。但要注意:如果你不 await 一个返回 Promise 的调用,它的 reject 就不会被任何 try/catch 捕获——翻译官没上班,坏消息没人传达。

如果你不调用 await,就像顾客已经从菜单上看到“售罄”却没人来通知——问题不会消失,只是变成了“未处理的 Promise 拒绝”(unhandledrejection),在 Node.js 里甚至可能直接终止进程。所以请记住:await 这位翻译官必须在场,坏消息才能被妥善传达。

await p={vif p resolvesthrow eif p rejects\text{await } p = \begin{cases} v & \text{if } p \text{ resolves} \\ \text{throw } e & \text{if } p \text{ rejects} \end{cases}
await 是异步与同步之间的翻译官,把 reject 翻译成 throw,把 resolve 翻译成返回值。
  1. 关键字 await 暂停遇到 await 时 async 函数挂起,交出控制权,等待 p 的状态落定
    suspend until p settles\text{suspend until } p \text{ settles}
  2. resolve 解包若 p 状态为 fulfilled,将其值 v 解包出来,继续往下执行
    v←p.valuev \leftarrow p.\text{value}
  3. reject 抛出若 p 状态为 rejected,将原因 e 作为异常抛出,交给最近的 catch
    throw e,e←p.reason\text{throw } e, \quad e \leftarrow p.\text{reason}

并行await处理

顾客一口气点了三道菜。后厨只有一个灶台,一道一道炒,效率太低;多个灶台同时开火,效率高但风险也大——如果一道菜做糊了,是整桌菜全倒掉,还是只重做那一盘?这就是并行异步错误处理的核心选择。

JavaScript 提供两把不同的“厨具”。Promise.all 是“一损俱损”模式:任何一个任务失败,整个调用立刻以失败告终,其他还在进行的任务的结果全部丢弃。Promise.allSettled 是“各自安好”模式:等待所有任务完成,无论成败,逐个返回每项的状态,失败的任务用 status: 'rejected' 标识,不干扰其他任务的成功结果。

如果任务之间完全独立,allSettled 是更安全的选择;如果后续逻辑依赖所有结果(比如必须同时拿到用户信息和订单才能渲染页面),用 Promise.all 反而能在失败时快速失败,避免无谓等待。

就像餐厅经理决定:如果顾客点的是“套餐”(所有菜必须一起上),一道失败就重做整桌(Promise.all);如果点的是“单点”(各吃各的),一道失败就只补那一盘,其余照常上(allSettled)。选对工具,才能在并行场景下优雅地处理错误。

Promise.all(p1,…,pn) rejects  ⟺  ∃i:pi rejects\text{Promise.all}(p_1,\dots,p_n) \text{ rejects} \iff \exists i : p_i \text{ rejects}
Promise.allSettled(p1,…,pn)=[{statusi,valuei/reasoni}]i=1n\text{Promise.allSettled}(p_1,\dots,p_n) = [\{\text{status}_i, \text{value}_i/\text{reason}_i\}]_{i=1}^n
all 是“一票否决制”:任何一个任务失败整体就失败;allSettled 是“记录式投票”:每个任务各得其所,失败不拖垮成功。
  1. all 的失败传播只要存在某个 i 使 p_i 被拒绝,整个 Promise.all 立即进入 rejected 状态
    ∃i:pi=rejected⇒all rejected\exists i : p_i = \text{rejected} \Rightarrow \text{all rejected}
  2. allSettled 的完整性等待全部 p_i 完成,返回一个由每项状态(fulfilled 或 rejected)和对应值或原因组成的数组
    resulti={status,value or reason}\text{result}_i = \{ \text{status}, \text{value or reason} \}
js11 行
// 一损俱损try {  const [a, b] = await Promise.all([getUser(), getOrders()]);  // 任一个失败都会走到 catch} catch (e) { /* 整体失败 */ } // 各自安好const [userResult, orderResult] = await Promise.allSettled([getUser(), getOrders()]);if (userResult.status === 'rejected') {  // 只处理 user 失败, order 结果不受影响}

统一错误处理

小餐厅里,每个服务员都自己处理投诉,处理方式五花八门:有的赔礼道歉,有的直接免单,有的假装没听见。顾客体验全看运气。但大型餐厅会设置一位值班经理——所有投诉统一上报,由经理分类、记录、制定补偿方案。代码里的统一错误处理工具,就是那位值班经理。

在实际项目中,如果每个请求都手写 try/catch,会产生大量重复代码:错误日志格式不统一、用户提示不一致、部分异常被静默吞掉。更糟的是,不同页面处理的错误风格可能截然不同。解决方案是封装一个统一的错误处理包装器(wrapper),通过它包裹所有异步请求,让异常统一流经同一处理管线。

通过包装器,错误处理从“各处散落”变成“一处集中”:分类、上报、提示语生成都在一个函数里完成,调用方拿到了一个稳定、可预期的返回结构。这种方式特别适合团队协作——不同开发者写出的请求代码,行为完全一致。

有了值班经理,无论哪个服务员(业务代码)遇到问题,都会走同一套流程:先分类(是食材问题还是顾客问题)、再记录(日志)、最后给出统一话术(用户提示)。顾客体验稳定了,后厨的问题也清晰了——这才是生产级代码该有的错误处理形态。

wrap(f)(x)={{ok:true,data}if f(x) succeeds{ok:false,error:classify(e)}if f(x) rejects\text{wrap}(f)(x) = \begin{cases} \{ok: true, data\} & \text{if } f(x) \text{ succeeds} \\ \{ok: false, error: \text{classify}(e)\} & \text{if } f(x) \text{ rejects} \end{cases}
统一包装器像值班经理的接待流程:成功就正常放行,失败就分类登记后再反馈。
  1. 包装器接收异步函数wrap(f) 接收一个返回 Promise 的函数 f,返回一个处理过错误的新函数
    wrap(f):x↦new Promise\text{wrap}(f) : x \mapsto \text{new Promise}
  2. 成功路径f(x) 成功时,包装结果包含 ok: true 和原始数据 data
    {ok:true,data:v}\{ok: true, data: v\}
  3. 失败路径f(x) 失败时,捕获异常并交给 classify 分类,返回 ok: false 和错误信息
    {ok:false,error:classify(e)}\{ok: false, error: \text{classify}(e)\}
js15 行
// 统一的错误处理包装器async function safeRequest(promiseFn) {  try {    const data = await promiseFn();    return { ok: true, data };  } catch (err) {    const normalized = classifyError(err);   // 分类: 网络/业务/未知    reportError(normalized);                 // 上报: 日志 + 监控    return { ok: false, error: normalized.message };  }} // 调用方只需关心返回结构const res = await safeRequest(() => fetchUser(uid));if (!res.ok) showToast(res.error);

重试机制

服务员端着一盘菜去 3 号桌,半路被顾客撞翻。这盘菜本身没问题,是意外。经理说:再做一盘。但要注意——如果后厨连续三次做这道菜都失败,那就不是意外了,是这道菜本身有问题。这时候再让后厨重做就是浪费食材。另外,重做之间的间隔也有讲究:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,防止所有桌都同时喊“重做”,把后厨压垮。

重试机制适用于“瞬时故障”:网络抖动、服务器短暂过载、超时等。但如果错误是“确定性失败”——比如 401 未授权、参数格式错误——重试毫无意义。设计重试策略要回答三个问题:什么错误可以重试?最多重试几次?每次等待多久?幂等性是前提:如果操作本身不幂等(比如重复扣款),重试就是灾难。

指数退避(Exponential Backoff)是经典的等待策略:第 n 次重试前等待 baseDelay × 2^n 毫秒,并加上随机抖动(jitter)。抖动是什么?想象 100 个请求同时失败、同时重试,会造成“重试雪崩”——服务端刚喘过一口气,又被 100 个重试请求打趴。加上随机偏移后,重试请求在时间上均匀散开,服务端才有真正的喘息机会。

重试策略像餐厅的“出品保障制度”:第一次失败,可以立即重做;连续失败,就要停下来检查菜谱(判断错误类型);等待时间逐渐拉长,是为了给后厨恢复的时间。最关键的是——如果顾客说“不要香菜”厨师还放香菜(操作不幂等),重做多少次都没用。

tn=t0×2n+jitter,n=0,1,2,…t_n = t_0 \times 2^n + \text{jitter}, \quad n = 0, 1, 2, \dots
等待时间指数增长并掺入随机抖动,既给服务恢复的时间,又避免重试风暴。
  1. 基础等待时间t₀ 是最初的等待时长,比如 100ms,由业务场景决定
    t0=initial delayt_0 = \text{initial delay}
  2. 指数增长因子第 n 次重试前等待 t₀ × 2ⁿ,即 1 倍、2 倍、4 倍、8 倍递增
    2n:n=0,1,2,…2^n : n = 0, 1, 2, \dots
  3. 随机抖动加上一个随机量 jitter,让同一时刻的重试请求在时间上分散开
    jitter∼Uniform(0,full_jitter)\text{jitter} \sim \text{Uniform}(0, \text{full\_jitter})
js11 行
async function fetchWithRetry(fn, { maxRetries = 3, baseDelay = 100 } = {}) {  for (let attempt = 0; attempt < maxRetries; attempt++) {    try {      return await fn();    } catch (err) {      if (!err.retryable || attempt === maxRetries - 1) throw err;      const delay = baseDelay * 2 ** attempt + Math.random() * 50; // 指数 + 抖动      await sleep(delay);    }  }}

优雅降级

餐厅最招牌的“佛跳墙”今天做不了。高明的经理不会直接告诉顾客“没有”然后让顾客扫兴离开,而是提供替代方案:换成同级位的套餐、送一张下次可用的优惠券、或者先上例汤垫着。顾客体验打了折扣,但至少没有完全失望。这就是软件世界的优雅降级:主路径失败时,提供一条体面的退路。

在异步请求中,当主数据源不可用时,降级策略包括:读取本地缓存返回旧数据、使用合理的默认值填充 UI、切换到备用接口或 CDN、甚至返回一个“部分可用”的降级响应。关键是:降级不是“隐藏错误”,而是把体验损失控制在可接受范围,同时明确告知调用方“当前是降级数据”。

使用降级标记(degraded: true)至关重要——它让上层 UI 知道当前展示的非实时数据,可以展示“离线模式”“数据可能延迟”的提示。没有标记的降级是“瞒报”,前端把旧数据当新数据展示,会引发更严重的业务问题。

就像餐厅经理不会让顾客空着肚子离开,优秀的错误处理也不会让用户面对白屏。佛跳墙没了,就来一份同等用心的套餐——虽然不完全一样,但至少这顿饭还能吃下去。优雅降级的本质,是让失败变得体面。

result(x)={primary(x)主调用成功fallback(x)主调用失败\text{result}(x) = \begin{cases} \text{primary}(x) & \text{主调用成功} \\ \text{fallback}(x) & \text{主调用失败} \end{cases}
降级是主路径失败后的预案分支,目标是让可用性从 0 分拉回 80 分。
  1. 主调用优先先调用 primary(x),通常是真实接口或实时数据源
    primary(x)→success?\text{primary}(x) \to \text{success?}
  2. 成功则返回主调用成功,直接返回结果,不发生降级
    return primary(x)\text{return primary}(x)
  3. 失败进入降级主调用失败,执行 fallback(x),返回缓存、默认值或备用源的结果
    fallback(x)→degraded result\text{fallback}(x) \to \text{degraded result}
js9 行
async function fetchProduct(id) {  try {    return await fetchFromServer(id);          // 主方案  } catch (err) {    const cached = cache.get(id);              // 降级 1: 缓存    if (cached) return { ...cached, degraded: true };    return { id, name: '商品名称暂不可用', price: null, degraded: true }; // 降级 2: 默认值  }}

AI 应用场景

前端 API Client 统一错误处理封装

阶段:前端数据请求层开发

解决的问题:传统方式每个页面重复 try/catch,错误提示不统一,有的页面把错误吞掉导致黑屏无从排查。AI 方式用统一包装器 + 错误分类 + 监控上报,让所有请求的错误行为完全一致。

具体案例:某电商 App 的 API Client 封装了 safeRequest 函数。商品详情页请求失败时,先尝试重试 1 次;重试仍失败则读取本地缓存展示商品信息,并在页面顶部提示“网络异常,展示缓存数据”。运营后台通过错误上报中心实时看到接口失败率和失败分布。

Node.js 微服务调用链的容错设计

阶段:后端服务开发

解决的问题:微服务间调用存在网络超时、依赖服务过载等故障,错误处理不当会沿调用链向上传播导致雪崩。AI 方式用指数退避重试 + 熔断降级 + 统一错误码映射,让调用链在故障中保持可控。

具体案例:订单服务调用支付服务超时,按照指数退避策略重试 3 次(间隔 100ms、200ms、400ms),期间校验请求幂等性避免重复扣款。重试全部失败后,降级为“先锁单、稍后异步支付”,并返回明确的降级状态给前端展示。支付恢复后,补偿流程自动处理未完成订单。

AI 应用调用大模型 API 的容错与降级

阶段:AI 应用集成开发

解决的问题:LLM API 经常遇到限流(429)、超时(408)、服务不可用(503)等瞬时故障,直接崩溃会让对话机器人体验极差。用 async/await 错误处理包装大模型调用,失败重试、降级到小型模型或缓存答案,保证 AI 服务可用性。

具体案例:基于 GPT 的客服机器人调用大模型生成回复,遇到 429 限流时等待 1 秒重试,重试 2 次仍失败则降级到本地语料库的相似问题回答,并在响应中标记 degraded: true。前端据此展示“当前为离线知识库回答”提示,用户依然得到了帮助。

Python 演示

python48 行
import asyncio  class DishError(Exception):    """模拟 Promise.reject 的业务异常"""  async def fetch_dish(dish_name, should_succeed):    """模拟异步接口请求, 成功则返回菜品, 失败则抛异常"""    await asyncio.sleep(0.05)    if not should_succeed:        raise DishError(f"厨房做不了{dish_name}")    return f"{dish_name}出锅"  async def fetch_with_retry(dish_name, succeed_on, max_retries=3):    """指数退避重试: 每次失败后等待 0.1, 0.2, 0.4 秒"""    for attempt in range(max_retries):        ok = succeed_on[attempt] if attempt < len(succeed_on) else False        try:            dish = await fetch_dish(dish_name, ok)            return dish        except DishError as err:            wait = 0.1 * (2 ** attempt)            print(f"  第{attempt + 1}次失败: {err}, {wait:.1f}秒后重试")            await asyncio.sleep(wait)    raise DishError(f"{dish_name}三次重试仍失败")  async def main():    print("顾客点餐, 厨房同时开工...")    results = await asyncio.gather(        fetch_with_retry("宫保鸡丁", [True]),        fetch_with_retry("麻婆豆腐", [False, True]),        fetch_with_retry("蒸鱼", [False, False, False]),        return_exceptions=True,    )    final_dishes = []    for item in results:        if isinstance(item, Exception):            final_dishes.append("白米饭(降级兜底)")        else:            final_dishes.append(str(item))    print("最终上桌:", final_dishes)  if __name__ == "__main__":    asyncio.run(main())
目的:用 Python asyncio 复刻 async/await 错误处理的核心模式——try/catch 捕获、并行任务隔离、指数退避重试、失败降级,帮助理解 JS 中同样的思想。
  1. async def fetch_dish(dish_name, should_succeed)模拟一个可能失败(reject)的异步接口,对应 JS 中返回 Promise 的请求函数
  2. try:` 与 `except DishError as err对应 JS 的 try/catch,捕获 await 抛出的异常
  3. wait = 0.1 * (2 ** attempt)指数退避计算:0.1s、0.2s、0.4s,对应重试机制中的等待策略
  4. results = await asyncio.gather(..., return_exceptions=True)并行执行三个任务且互不干扰,对应 Promise.allSettled 的隔离语义
  5. if isinstance(item, Exception)判断某个任务是否失败,失败则走降级分支返回“白米饭(降级兜底)”
  6. if __name__ == "__main__":程序入口,调用 asyncio.run 启动事件循环

常见误区

误区写了 try/catch 包裹整个 async 函数,就以为所有异步错误都能被捕获。结果 setTimeout 回调里的错误还是把程序崩了,觉得很不可思议。

正解try/catch 只在执行流经过它时才有效。setTimeout、事件监听、未被 await 的 Promise 都在 try 的作用域之外。想捕获回调错误,应在回调内部写 .catch,或用全局 unhandledrejection 监听兜底。就像服务员只能接住自己管辖区域内的投诉,没法处理已经走到餐厅外的客人的问题。

为何常见:初学者把 try/catch 想象成“全局保护罩”,忽略了 JavaScript 执行流的异步边界——错误发生在哪个执行上下文,由哪个上下文负责。

误区有人以为 Promise.all 里每个 promise 都写了 .catch 就不会整体 reject 了,结果 Promise.all 还是走到了 catch 分支。

正解Promise.all 的“一损俱损”判断的是传入的 promise 数组的最终状态。如果每个子 promise 内部 catch 后返回了一个正常值,那整个 all 是成功;但如果 catch 后仍然 throw(或子 promise 本身拒绝未处理),all 就会整体失败。理解“子 promise 的状态是否已收敛”比“写没写 catch”更重要。

为何常见:混淆了“子任务内部处理错误”与“外层容器感知错误”两个层次,不清楚 Promise 的状态一旦确定就不变这一根本机制。

误区以为重试次数越多成功率越高,于是设置 maxRetries = 10。结果服务端被重试请求打爆,原本只崩溃一次的服务,因为 10 倍流量的重试彻底雪崩了。

正解重试必须结合指数退避 + 抖动 + 合理上限。重试 10 次意味着最大 11 倍流量放大。更要判断错误类型:业务错误(4xx)重试无意义,只有瞬时故障(网络抖动、超时、5xx)才值得重试。幂等性也是重试的前提——非幂等操作重试等于重复下单。

为何常见:把重试等同于“努力就会有回报”,忽略了分布式系统中重试的放大效应——你以为在修复问题,实际在制造更大的问题。

误区接口失败时 catch 里直接 return null,以为这就是降级。结果页面渲染 null 照样白屏,只是错误从控制台转移到了 UI 上。

正解优雅降级的核心是“体验止损”而非“错误隐藏”。降级要提供有价值的替代内容:缓存数据、默认值、备用接口。同时通过 degraded 标记让上层感知“当前非最佳状态”。如果直接 return null,UI 没有兜底依然白屏,错误只是提前消失了。

为何常见:把降级错误地理解为“消除错误的表现”,而没有理解降级的本质是“提供备选的可用内容”——就像餐厅不能说“佛跳墙没了”就把顾客晾在那里,而要给一个替代方案。

面试题

Q1. 使用 async/await 时,await 的 Promise 失败需要用什么结构来捕获?

单选题 · easy

  • A.for/while 循环遍历
  • B.switch/case 多路分支
  • C.try/catch 异常捕获← 正确答案
  • D.if/else 条件分支

答案:C。async/await 将 reject 自动展开为 throw,而 throw 的异常只能用 try/catch 来捕获。if/else、switch/case 是普通流程控制,不处理异常。for/while 是循环语句,与异常捕获无关。这就像餐厅里只有“值班经理”(catch)才能处理顾客投诉,其他岗位各有分工。

Q2. 无论成功或失败都必须执行的清理工作,应放在哪个代码块?

单选题 · easy

  • A.try 尝试块
  • B.catch 捕获块
  • C.throw 抛出语句
  • D.finally 收尾块← 正确答案

答案:D。finally 块无论 try 正常完成还是 catch 捕获异常后都会执行,是清理工作(关闭连接、清除 loading 状态)的标准位置。try 只在成功路径执行,catch 只在失败路径执行,throw 只是抛出异常,都不满足“无论成败都执行”的要求。

Q3. Promise.all 中只要有一个 Promise 失败,整体会?

单选题 · easy

  • A.继续等待所有结果
  • B.立即进入 rejected 状态← 正确答案
  • C.自动忽略失败结果
  • D.自动重试失败任务

答案:B。Promise.all 采用“一票否决制”——只要有一个子 Promise 进入 rejected 状态,整体立即 rejected,不会等待其他任务完成。这是它与 Promise.allSettled 的关键区别。想隔离失败,应改用 allSettled 或对每个任务单独加 catch。

Q4. 执行 async function f(){ try { await Promise.reject('err'); } catch(e){ console.log('caught:', e); } } f(); 的输出结果是?

单选题 · mid

  • A.抛出未捕获的拒绝异常
  • B.输出 caught: err← 正确答案
  • C.程序直接崩溃退出
  • D.什么也不输出

答案:B。await 会把 Promise.reject('err') 翻译成 throw 'err',try/catch 捕获后打印出 "caught: err"。这正是 await 的核心价值:让异步失败像同步异常一样可捕获。如果去掉 catch,才会变成未处理的 Promise 拒绝,可能导致进程崩溃。

Q5. 两个互相独立的异步请求需要同时发起,且一个失败不应影响另一个,应该使用?

单选题 · mid

  • A.串行 await 依次请求
  • B.Promise.all 合并等待
  • C.Promise.allSettled 隔离失败← 正确答案
  • D.嵌套 try/catch 包裹

答案:C。Promise.allSettled 会等待所有任务完成,返回每项的状态,一个失败不拖垮其他成功任务。串行 await 性能差,Promise.all 是“一损俱损”,嵌套 try/catch 无法实现真正的并发隔离。这就像顾客点了三道独立单点的菜,一道失败只影响那一盘。

Q6. 指数退避重试策略中,等待时间逐步递增的核心目的是?

单选题 · mid

  • A.提高请求的成功率
  • B.减少需要重试的次数
  • C.避免重试风暴压垮服务← 正确答案
  • D.简化代码逻辑复杂度

答案:C。指数退避让重试间隔呈 1 倍、2 倍、4 倍递增,核心目的是避免大量客户端在失败瞬间同时重试,形成“重试风暴”压垮服务端。成功率提升是“服务端恢复”的副产品,不是等待时间递增本身直接决定的。

Q7. 执行 async function f(){ try { await Promise.reject('e'); } finally { console.log('finally'); } } f(); 的结果是?

单选题 · hard

  • A.函数无 catch 则不执行
  • B.在异常抛出到外层之前执行← 正确答案
  • C.在 catch 块执行完之后执行
  • D.在整个程序结束时才执行

答案:B。finally 在 try 块执行完毕后立即执行——无论 try 是正常完成还是因异常中断。本例没有 catch,异常会在 finally 执行后被抛出到函数外(返回 rejected Promise),但 "finally" 已经打印。finally 是收尾兜底,不是错误处理本身。

Q8. 关于 async/await 错误处理的说法,正确的有哪些?

多选题 · mid

  • A.未捕获的 reject 可能让进程崩溃← 正确答案
  • B.allSettled 不会因单任务失败而中断← 正确答案
  • C.try/catch 能捕获所有异步错误
  • D.async 内 throw 等价于返回 reject← 正确答案

答案:ABD。未处理的 Promise 拒绝在 Node.js 中会触发 unhandledRejection,可能导致进程退出;allSettled 等待所有任务完成,失败任务仅记录状态;async 函数内 throw 确实等价于返回 rejected Promise。但 try/catch 无法捕获 setTimeout 回调等脱离执行流的错误,C 错误。

Q9. 以下哪些属于优雅降级的实现方案?

多选题 · hard

  • A.接口失败时返回缓存数据← 正确答案
  • B.核心服务异常时展示默认值← 正确答案
  • C.失败后切换备用服务器源← 正确答案
  • D.捕获错误后静默吞掉所有异常

答案:ABC。缓存兜底、默认值填充、备用源切换都是“提供备选可用内容”的正确降级策略。D 是反模式——吞掉异常不等于降级,它让问题无法被感知和排查。降级的本质是给用户一个体面的替代方案,而不是把错误藏起来。

Q10. 设计生产级异步错误处理方案时,正确的做法是?

多选题 · extreme

  • A.统一错误分类并映射用户提示← 正确答案
  • B.网络抖动场景使用指数退避重试← 正确答案
  • C.每个异步调用都 try/catch 并吞错
  • D.关键链路失败时提供降级数据方案← 正确答案

答案:ABD。生产级方案需要:统一分类错误并生成一致的用户提示、对瞬时故障用指数退避重试(含抖动和上限)、关键链路失败时降级到缓存或默认值。C 是严重的反模式——吞掉异常会让错误完全不可见,排查问题时像在黑夜里找一根针,是新手最容易犯的错误。