从一次
Promise.all与Promise.allSettled的选择,谈谈 AI 编程中的场景判断、业务边界和开发者责任。
前言
现在,Cursor、GPT、DeepSeek、豆包等 AI 工具,已经逐渐成为程序员日常开发的一部分。
写 CRUD、补类型、生成表单、封装请求、解释陌生代码,AI 确实能够显著减少重复劳动。
但在真实项目中,我逐渐发现了一个比“AI 会不会写错语法”更值得警惕的问题:
AI 给出的代码可能语法正确、能够运行,甚至符合通用最佳实践,但放进具体业务后,仍然可能是错误的方案。
这种错误往往比语法错误更隐蔽。
语法错误通常会直接报错,而场景不匹配的代码,可能在正常情况下运行得很好,直到某个接口失败、某个数据为空、某个用户进行特殊操作时,问题才暴露出来。
近期,我在处理一个多字典并发请求的业务时,就遇到了这样的问题。
一、四款 AI 都推荐了 Promise.all
当时的业务场景是:
一个后台表单初始化时,需要同时请求多个下拉字典接口,例如证件类型、医保类型、人员类别、结算类型等。
这些字典之间没有严格依赖关系。
业务要求也很明确:
- 多个字典接口需要并发请求;
- 某一个字典接口失败,不能阻断其他字典加载;
- 已成功返回的字典仍然需要正常展示;
- 失败的字典可以单独记录、提示或重新请求;
- 页面可以在部分数据可用的情况下继续运行。
我先后使用了 GPT、豆包、DeepSeek 和 Cursor 辅助分析,得到的初始方案高度一致:
const results = await Promise.all([
getDictA(),
getDictB(),
getDictC(),
getDictD(),
]);
从代码本身来看,这段实现没有语法问题,Promise.all 也确实是非常常见的并发请求方案。
但它不符合当前业务的失败策略。
只要其中一个 Promise 进入 rejected 状态,Promise.all 返回的 Promise 就会立即进入 reject 分支:
try {
const results = await Promise.all([
getDictA(),
getDictB(),
getDictC(),
]);
} catch (error) {
console.error('字典加载失败', error);
}
需要特别说明的是:
Promise.all进入 reject,并不代表其他已经发出的网络请求会自动终止。
其他请求通常仍然会继续执行,只是调用方无法再通过这次 Promise.all 正常获得一个包含全部成功结果的数组。
除非额外使用 AbortController 等机制,否则已经发出的请求不会因为 Promise.all 失败而自动取消。
这也意味着,我的问题不是“请求有没有继续执行”,而是:
当部分请求失败时,系统是否还需要保留并处理其他成功结果?
在当前业务中,答案是需要。
因此,我最后选择了 Promise.allSettled:
const results = await Promise.allSettled([
getDictA(),
getDictB(),
getDictC(),
getDictD(),
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`第 ${index + 1} 个字典加载成功`, result.value);
} else {
console.error(`第 ${index + 1} 个字典加载失败`, result.reason);
}
});
Promise.allSettled 会等待所有 Promise 都完成,并分别保留每一项的成功或失败状态。
这样,一个字典失败不会阻断其他字典的处理,更符合当前业务“允许部分成功”的要求。
二、真正的问题不是选哪个 API
如果只停留在表面,这件事很容易被总结成:
Promise.allSettled比Promise.all更好。
但这显然是不准确的。
Promise.all 并没有错,Promise.allSettled 也不是任何场景下都更高级。
真正需要判断的是:
当前系统采用的是整体成功策略,还是允许部分成功策略?
适合 Promise.all 的场景
当多个结果共同组成一个不可拆分的整体时,任何一个请求失败,都意味着后续流程无法继续。
例如:
- 订单信息、订单明细和支付信息必须完整返回;
- 多项配置之间存在强依赖关系;
- 缺少任意一项都会导致页面展示错误;
- 业务要求数据必须全部准备完成后才能提交。
这时,使用 Promise.all 更符合语义:
const [order, details, payment] = await Promise.all([
getOrder(),
getOrderDetails(),
getPaymentInfo(),
]);
适合 Promise.allSettled 的场景
当任务之间相互独立,允许部分成功时,Promise.allSettled 更合适。
例如:
- 表单中的多个独立字典;
- 批量查询多个非关键数据源;
- 批量上传文件,需要分别展示成功与失败结果;
- 首页多个独立统计卡片;
- 多个第三方服务的聚合查询。
因此,这次技术选择的本质不是 API 记忆,而是:
接口之间是强依赖还是弱依赖?
系统应该整体失败,还是允许降级运行?
当问题被这样重新定义后,答案才真正清晰。
三、这算不算 AI 幻觉?
严格来说,这个案例不完全属于典型的“AI 幻觉”。
通常所说的技术幻觉,更接近以下情况:
- 编造一个不存在的 API;
- 虚构某个框架参数;
- 引用不存在的官方文档;
- 混淆不同版本的配置项;
- 对库的实际行为作出错误描述;
- 在被纠正后仍坚持错误事实。
而这一次,AI 推荐的 Promise.all 是真实存在的,也确实可以正常运行。
它的问题在于:
方案本身成立,但与具体业务场景不匹配。
更准确地说,这是一次:
- 场景误判;
- 通用模板与业务约束错配;
- 失败策略选择不当;
- 上下文不足导致的默认方案偏差。
这个区分非常重要。
如果把所有不合适的答案都称为幻觉,我们很容易忽略另一个事实:
AI 输出质量不仅与模型能力有关,也与开发者有没有把业务目标、约束和失败策略描述清楚有关。
四、为什么不同 AI 容易给出相同方案?
多个 AI 给出相同答案,并不意味着它一定适合当前业务。
它更多说明:
这个答案是一种常见、概率较高的默认实现。
1. Promise.all 是更常见的并发模板
在大量教程、博客、面试题和示例代码中,Promise.all 经常被用来演示并发请求。
因此,当问题只描述为“批量请求多个接口”时,模型很容易优先生成这种高频模式。
但“批量并发”只是表面需求,模型还需要知道:
- 请求之间是否相互依赖;
- 部分失败是否可以接受;
- 失败后是否需要保留成功结果;
- 页面是否允许降级;
- 是否需要重试失败项。
缺少这些信息时,AI 很可能只能给出通用答案。
2. AI 只能基于获得的上下文进行判断
AI 可以分析业务,但前提是业务信息已经被提供。
如果提示词只是:
帮我并发请求多个字典接口。
那么 Promise.all 并不是一个离谱答案。
但如果明确说明:
多个字典相互独立,任意一个失败都不能影响其他结果,需要保留成功数据并记录失败项。
那么更合理的方案就会发生变化。
所以,AI 并不是在独立理解整个项目,它只能根据:
- 当前对话;
- 提供的代码;
- 项目文件;
- 文档内容;
- 工具检索结果;
来推断真实需求。
它无法自动知道那些没有被表达出来的业务规则。
3. 多个模型可能共享相似的高频代码模式
即使不同产品使用不同模型,它们也可能学习过大量相似的公开代码、教程和文档。
所以多个模型得出相同结论,只能说明这个结论常见,不能替代业务验证。
模型共识不等于业务正确。
五、AI 编程中更值得警惕的四类问题
这次经历让我意识到,真正危险的并不只是不存在的 API,而是那些“看起来非常正确”的方案。
1. 场景与失败策略不匹配
除了 Promise.all 和 Promise.allSettled,类似问题还包括:
- 请求失败后是整体回滚还是保留部分结果;
- 缓存失效后是阻塞页面还是返回旧数据;
- 表单部分字段加载失败时是否允许继续编辑;
- 多个服务异常时系统如何降级;
- 批量任务失败时是立即停止还是继续执行。
这些问题没有脱离业务的标准答案。
AI 可以提供选项,但不能代替项目负责人承担决策责任。
2. 框架版本和项目环境不匹配
AI 可能出现:
- Vue 2 和 Vue 3 写法混用;
- React 新旧生命周期混用;
- 使用当前版本已经废弃的配置;
- 给出不适用于现有构建工具的插件配置;
- 忽略浏览器兼容范围;
- 推荐与项目依赖版本冲突的 API。
现在部分 AI 产品能够联网、读取官方文档或者分析项目代码,因此不能简单地说“AI 永远只能使用训练截止日期之前的知识”。
更准确的说法是:
模型自身记忆可能存在时间滞后;即使产品支持联网和文档检索,如果没有检索官方资料,或者没有结合当前项目版本,仍然可能输出过时方案。
因此,涉及版本问题时,应该优先核对:
package.json;- 锁文件;
- 官方文档;
- Release Notes;
- Migration Guide;
- 实际类型定义。
3. 正常流程完整,异常流程缺失
AI 很擅长生成 happy path:
const data = await request();
render(data);
但真实项目还要考虑:
- 请求失败怎么办;
- 返回
null怎么办; - 数组为空怎么办;
- 接口重复提交怎么办;
- 用户连续点击怎么办;
- 请求超时怎么办;
- 组件卸载后请求才返回怎么办;
- 权限变化怎么办;
- 部分数据成功怎么办。
代码能运行,只证明正常路径可以通过。
工程质量更多体现在:
异常发生时,系统能否以可控方式失败。
4. 数据结构和业务规则被过度简化
例如:
- 将重复 ID 直接当作 React 的
key; - 每次渲染生成随机数或时间戳作为
key; - 树形结构递归时忽略循环关系;
- 金额计算直接使用浮点数;
- 日期比较忽略时区;
- 权限判断只在前端完成;
- 列表去重忽略业务组合键;
- 批量数据处理忽略空值和重复数据。
这些代码往往能够通过简单测试,但放到生产数据中就可能出问题。
六、比“让 AI 直接写代码”更可靠的工作流
在与前辈交流时,有一句话让我印象很深:
先把方案归档,再让 AI 实施。
这句话背后的价值,不是要求每个小需求都写一份正式设计文档,而是强调:
不要让 AI 在需求不清、边界不明的情况下,一边猜方案,一边大范围修改代码。
我现在更认可下面这套流程。
第一步:先定义问题,不要立即写代码
至少先回答四个问题:
1. 当前现象是什么?
2. 真正目标是什么?
3. 有哪些不能被破坏的约束?
4. 异常发生时,系统应该怎么表现?
以本文案例为例:
现象:
页面初始化需要加载多个字典。
目标:
提高加载效率,同时保留可用结果。
约束:
字典相互独立,单个失败不能阻断整体。
失败策略:
成功项继续展示,失败项单独记录或重试。
写清楚这些内容后,技术方案通常已经完成了一半。
第二步:先让 AI 分析方案,不要立即修改代码
可以这样提问:
请先不要编写或修改代码。
当前场景:
页面初始化需要并发加载多个相互独立的字典接口。
业务约束:
1. 任意一个接口失败,不能影响其他接口结果;
2. 成功数据需要正常展示;
3. 失败项需要记录,并允许后续单独重试;
4. 页面允许部分可用。
请完成:
1. 分析问题的核心矛盾;
2. 提供至少两种实现方案;
3. 说明每种方案的适用条件、失败行为和风险;
4. 推荐一个方案并说明理由;
5. 构造可能使推荐方案失效的反例。
这样,AI 的角色从“直接生成代码”变成了“参与方案评审”。
第三步:主动构造反例
拿到方案后,继续追问:
在什么情况下,这个方案会失效?
对于 Promise.allSettled,也需要继续思考:
- 某些字典是不是实际上的核心字典?
- 核心字典失败后是否应该禁用提交?
- 是否需要区分关键接口和非关键接口?
- 所有请求都失败时页面如何处理?
- 是否需要设置超时?
- 失败接口是否应该自动重试?
- 字典加载完成前是否允许用户操作?
- 如何避免组件卸载后继续更新状态?
进一步看,真实方案可能不是简单地全部使用 allSettled,而是分组处理:
const [coreDicts, optionalDicts] = await Promise.all([
Promise.all([
getRequiredDictA(),
getRequiredDictB(),
]),
Promise.allSettled([
getOptionalDictC(),
getOptionalDictD(),
]),
]);
核心字典要求全部成功,非核心字典允许部分失败。
这比简单争论 all 和 allSettled 更接近真实工程设计。
第四步:让 AI 落地代码
方案确定后,再让 AI:
- 补充 TypeScript 类型;
- 封装通用函数;
- 处理错误日志;
- 增加重试逻辑;
- 编写测试用例;
- 检查影响范围。
例如:
type SettledResult<T> = {
success: T[];
failed: unknown[];
};
async function loadIndependentResources<T>(
tasks: Array<Promise<T>>,
): Promise<SettledResult<T>> {
const results = await Promise.allSettled(tasks);
return results.reduce<SettledResult<T>>(
(acc, result) => {
if (result.status === 'fulfilled') {
acc.success.push(result.value);
} else {
acc.failed.push(result.reason);
}
return acc;
},
{
success: [],
failed: [],
},
);
}
AI 很适合完成这些明确、可验证的实现工作。
第五步:人工终审和异常验证
上线前至少检查三个维度:
场景匹配
- 当前方案是否符合业务目标?
- 请求之间究竟是强依赖还是弱依赖?
- 部分成功是否可能造成错误数据?
边界覆盖
- 单个请求失败;
- 多个请求失败;
- 所有请求失败;
- 请求超时;
- 返回空数据;
- 重复点击;
- 页面提前卸载。
版本与环境
- API 是否适用于当前浏览器范围?
- 框架和依赖版本是否匹配?
- 是否需要 polyfill?
- 是否与项目中的请求封装冲突?
AI 可以帮助列出测试项,但最终验收责任仍然在人。
七、基础知识并没有失去意义
在这次交流里,还有一个值得讨论的观点:
有了 AI,以后是不是不需要太关注基础了?
我的答案是:基础知识的作用变了,但并没有消失。
以前学习基础,可能更强调:
- 记住 API;
- 手写语法;
- 背诵概念;
- 独立查询文档。
现在有了 AI,基础能力更多体现在:
- 判断 AI 的代码是否符合需求;
- 看懂不同方案的失败行为;
- 识别边界条件;
- 发现版本冲突;
- 判断性能、安全和维护成本;
- 在 AI 出错时找到原因。
如果不了解 Promise 的状态变化,就很难发现 Promise.all 不符合业务。
如果不了解 React 的协调机制,就可能接受每次渲染生成随机 key 的建议。
如果不了解前后端权限边界,就可能误以为前端隐藏按钮等于安全控制。
所以,AI 并没有让基础失去价值,而是让基础从:
“我能不能从零写出来”
转变为:
“我能不能判断它为什么这样写,以及这样写会不会出问题”。
八、AI 是工具,不是技术决策的责任主体
有人把 AI 比作“达摩克利斯之剑”。
我认为这个比喻有一定道理。
AI 会放大开发者的能力,也会放大开发者的错误:
- 有判断力的人,可以用 AI 快速验证方案、补充知识、减少重复劳动;
- 缺少判断的人,可能会把不合适的代码更快地扩散到整个项目。
因此,与其把 AI 定位为“绝对正确的专家”,不如把它看成:
执行效率很高、知识面很广,但需要明确上下文、持续校验和最终审核的协作者。
一种更合理的协作关系是:
人负责:
定义目标、识别约束、选择方案、评估风险、承担结果。
AI 负责:
搜索资料、解释代码、生成初稿、补充实现、构造测试、提高效率。
人最终负责:
审核、验证、上线和复盘。
AI 可以给出候选答案,但它不能替开发者承担生产事故,也无法自动拥有项目中所有未被表达的业务信息。
九、关于终端、TUI、Vim 和 GUI
交流中还提到了一个观点:
应该尽量减少 GUI,多使用终端、TUI 和 Vim。
我认同学习终端工具的价值,但不认为需要把 GUI 和终端做成二选一。
终端、TUI 和 Vim 能帮助我们:
- 熟悉命令和工具链;
- 提升批量处理效率;
- 进行远程服务器操作;
- 编写自动化脚本;
- 更清楚地理解 Git、构建和部署流程。
但 GUI 同样适合:
- 浏览器调试;
- 大型项目代码导航;
- 查看复杂 Git 提交关系;
- 数据库表结构分析;
- 可视化性能分析;
- 处理复杂合并冲突。
真正值得培养的不是“只用哪一种工具”,而是:
理解工具解决了什么问题,并根据场景选择成本最低的方式。
工具偏好不是问题本质,能否完成目标、控制风险、提高质量才是。
十、总结:代码正确,不代表方案正确
这次经历真正让我意识到的,并不是 Promise.allSettled 比 Promise.all 更好,而是:
开发者不能只检查代码是否能够运行,还要判断方案是否符合业务目标、依赖关系和失败策略。
可以将这次经验总结为五点:
- AI 给出的代码可能语法正确,但仍然可能与业务场景不匹配;
- 多个模型给出相同答案,只能证明方案常见,不能证明方案适合当前项目;
- 技术选择不能脱离目标、约束、依赖关系和失败策略;
- AI 更适合参与分析和执行,而不应该在上下文不足时独立主导方案;
- 程序员真正需要提升的,是定义问题、识别边界、评估方案和验证结果的能力。
以前,我更关注的是:
这段代码怎么写?
现在我会继续追问:
为什么这样写?
它解决的到底是什么问题?
在什么情况下会失效?
失败之后系统会怎样?
是否存在更符合当前业务的方案?
AI 降低了获取知识和编写代码的门槛,但它没有替开发者承担判断责任。
未来真正拉开差距的,可能不再是谁记住了更多 API,而是谁能够更准确地定义问题,并在多个看似正确的答案中,选择真正适合当前业务的方案。
附录:一次技术交流带来的反思
本文源于一次关于 AI 编程的真实交流。
对话中提到了 AI 的双刃剑属性、模型知识边界、开发者基础能力、终端工具,以及“先归档方案,再实施”等观点。
我并不完全认同其中所有结论,例如:
- 有 AI 以后基础就不重要;
- GUI 一定会拖累开发者;
- 多个 AI 给出错误方案一定是模型幻觉。
但这场交流仍然给了我一个很重要的启发:
技术成长不仅是解决眼前的 bug,更重要的是从一次问题中提炼出能够反复使用的判断方法。
相比于记住这次应该使用 Promise.allSettled,更有价值的是记住:
先明确目标
→ 再识别约束
→ 分析底层机制
→ 比较方案边界
→ 构造失败场景
→ 最后实施和验证
这可能才是从“会写代码”走向“能解决问题”的真正起点。