第3章 异步编程与错误处理
核心命题:回调地狱 → Promise 链 → async/await 演进路径;错误传播机制(冒泡 vs 显式捕获);并发控制与异常兜底。面试中通常以「手写场景 + 异常边界」组合考察。
面试问答卡 1:回调地狱及解决方案
| 面试问题 | 请解释什么是回调地狱?你如何解决它? |
| 考察点 | 对异步嵌套问题的认知;是否能给出 Promise、async/await 等具体改造方案。 |
| 回答框架 | ①定义回调地狱(嵌套+缩进爆炸+错误丢失)→②核心问题:可读性差、错误传播困难 →③解决方案:先重构为 Promise 链,再优化为 async/await + try/catch →④额外提工具:async 库、co、或 @supercharge/promise-pool。 |
| 示例回答 | “回调地狱指多层回调嵌套导致的代码横向膨胀。我一般先提取函数名用 Promise 包裹,然后用 async/await 改写,配合 try/catch 统一捕获错误。若大量并发,使用 Promise.allSettled 替代 Promise.all 防止单次失败全部终止。” |
| 可能追问 | Promise.all 与 Promise.allSettled 区别?async/await 内部如何被编译? |
| 避坑提醒 | 不要只讲理论,需举具体业务场景(如数据库事务、文件读取)。避免说出“await 会阻塞”这种错误表述。 |
面试问答卡 2:async/await 错误捕获策略
| 面试问题 | async/await 中如何全面捕获异步错误?请手写一个可靠模式。 |
| 考察点 | try/catch 覆盖范围;全局未捕获处理;是否了解 Promise 错误反向冒泡。 |
| 回答框架 | ①每个函数内 try/catch 包裹 await →②最外层添加 process.on('unhandledRejection') →③若需更高可靠性,使用 withTry 高阶函数包装(async fn => { try { return [null, await fn()] } catch(e) { return [e] } }) →④生产环境配合 sentry 上报。 |
| 示例回答 | “我的常用模式:①函数内用 try/catch 包围所有 await;②全局设置 unhandledRejection 监听;③编写一个 safeAwait 工具函数返回 [err, data] 元组。这样既能局部处理也能全局兜底,避免未捕获 Promise 错误导致进程崩溃。” |
| 可能追问 | 如何设计重试机制?如何区分业务错误与系统错误? |
| 避坑提醒 | 不要在 async 函数内部遗漏 catch;注意 process.on('uncaughtException') 会阻止进程退出但可能导致内存泄漏。 |
面试问答卡 3:Promise 链错误传播与中断
| 面试问题 | Promise 链中某个 .then 抛出错误后后续 .then 还会执行吗?如何中断链? |
| 考察点 | Promise 错误冒泡机制;catch 位置的影响;链式调用控制。 |
| 回答框架 | ①错误发生后跳过所有后续 .then,直到最近的 .catch →②如果在 .catch 后继续 .then,可恢复链 →③想中断:在 .catch 中 return Promise.reject() 或 throw 错误 →④使用 reject 后不能通过 throw 继续回调(需特殊处理)。 |
| 示例回答 | “错误会在 Promise 链中跳过所有 .then 直到 catch。若需要在 catch 后停止,应 return Promise.reject(new Error()) 或直接 throw。注意 finally 总会执行,适合清理资源。我曾在批量文件上传项目中利用这一特性做失败重试。” |
| 可能追问 | Promise.finally 与 try/catch/finally 的区别?如何同时捕获多个 Promise 的错误? |
| 避坑提醒 | 勿把 .catch 放在链中间又缺少后续处理;使用 Promise.all 时其中一个失败会导致所有结果丢失,改用 allSettled 按需处理。 |
项目案例:高并发订单处理服务
| 项目名称 | 异步订单合规校验与结算引擎 |
| 业务背景 | 电商平台每笔订单需经过 5~8 个异步合规校验(风控、库存、价格、优惠券、物流),单日峰值订单量 80 万笔。 |
| 规模指标 | 并发 2000+ QPS;每个校验请求平均耗时 150ms;全链路超时阈值 2s。 |
| 技术栈 | Node.js 16 + Express + MySQL + Redis + RabbitMQ + axios(下游RPC) |
| 核心难点 | ①并发校验时任一失败需快速回滚并释放资源;②超时控制与重试隔离;③防止回调地狱,错误日志可追溯。 |
| 面试高频追问 | “你怎么避免一个校验失败导致所有等待的 Promise 内存泄漏?”“如何处理部分失败但不想重试的非关键校验?” |
| 可迁移回答话术 | “我用 Promise.allSettled + 超时 Promise.race 处理并发校验。将关键校验放在一个组内(全部成功才继续),非关键校验单独处理并记录错误。异步错误通过统一 wrap 函数转为 [err, data] 格式,结合全局 unhandledRejection 日志上报,确保生产环境零未捕获异常。” |
💡 讲师提示:本项目中「Promise.allSettled + 超时 Promise.race」模式是业界标准做法,面试时主动提出会比只说 Promise.all 更具竞争力。
⚠️ 高频陷阱:面试官常问“如何中断 async/await 中的异步操作”,注意 async/await 本身不支持中断,需借助 AbortController 或提前 resolve 一个 Promise 来模拟。