在 async 函数中做防御性编程,核心是把“可能出错”当作默认前提,需主动校验输入、限制外部依赖边界、避免竞态条件、统一结构化错误抛出。

在 async 函数中做防御性编程,核心是把“可能出错”当作默认前提,而不是例外。这意味着不能只靠 try/catch 被动兜底,而要主动检查输入、约束执行边界、隔离外部依赖、明确失败语义。
校验输入与提前拒绝
async 函数常作为 API 入口或业务协调层,若参数非法(如空 ID、无效邮箱、超出范围的分页页码),应同步拦截,避免无谓发起异步调用。
- 用普通 if 或自定义校验函数做同步检查,错误直接 throw(不 await)
- 对 Promise 类型参数,先
if (!(arg instanceof Promise)) throw new TypeError(...),防止传入 thenable 导致意外行为 - 对可选参数设默认值时,优先用
??而非||,避免 0、false、'' 等 falsy 值被误覆盖
限制外部调用的可靠性边界
HTTP 请求、数据库查询、RPC 调用等外部依赖天然不可靠。防御性做法不是等待它变可靠,而是定义它“多不可靠”,并控制影响范围。
- 加超时:用
Promises.race([fetch(...), timeout(5000)])或封装带 timeout 的 fetch - 设重试上限与退避:失败后延迟再试,但最多 2~3 次,避免雪崩;指数退避比固定延迟更稳妥
- 降级处理:超时或 404 时返回缓存数据、兜底值或空数组,而非让整个流程中断
避免隐式状态与竞态条件
async 函数内部若共享变量(如闭包中的计数器、缓存 map)、或多次 await 同一资源却未同步,容易引发竞态。防御的关键是让状态变更显式、原子、可预测。
- 对需并发保护的操作(如“读-改-写”),用
await lock.acquire()+finally { lock.release() }(借助第三方库如async-mutex) - 避免重复触发同一异步任务:用 memoize 工具缓存 Promise 实例(不是结果),确保相同参数只发起一次请求
- 不用
let在循环中捕获索引(for (let i = 0; i console.log(i), 100)是安全的;但用var或在forEach里 await 就易出错)
统一错误分类与结构化抛出
throw new Error('Network failed') 对调试友好,但对上层决策无用。防御性编程要求错误携带语义,方便 caller 区分处理。
- 定义错误类继承 Error,如
class ValidationError extends Error、class ServiceUnavailableError extends Error - 在 catch 中不简单 re-throw,而是包装补充上下文:
catch (err) { throw new DatabaseQueryError('Failed to save user', { cause: err, userId: id }); } - 对业务逻辑错误(如余额不足),用特定错误类型而非 status code 判断,避免 HTTP 层细节泄漏到业务层











