Promise 不是“让代码异步执行”的按钮,而是一个代表未来结果的对象。它把成功值和失败原因沿链传递;
try/catch则处理当前调用链中的抛出,await把 Promise rejection 重新接回当前 async 函数的错误流。
我追问的链
new Promise(...)里的代码是立即执行,还是以后执行?- Promise 为什么只有三个状态?状态能改回去吗?
then为什么总是返回一个新 Promise?then返回普通值、Promise 或抛出异常,后面分别收到什么?catch到底捕获的是谁的错误?try/catch是否必须遇到throw才会执行?- 为什么 timer、事件和没有
await的 Promise 捕获不到? return new Error()和throw new Error()有什么区别?Promise.all、allSettled、race、any应该怎么选?
1. Promise 是未来结果的容器
普通函数立即返回一个值:
1 | function getUser() { |
网络请求暂时没有结果,所以先返回一个 Promise:
1 | function getUser(): Promise<User> { |
Promise 表达的是:
1 | 现在:结果还没出来 |
调用方不用知道网络、文件、定时器还是 native bridge 在工作,只需要围绕这个未来结果继续组织代码。
2. Promise 状态机
Promise 只有三种状态:
1 | pending |
pending:结果尚未确定。fulfilled:成功,并保存一个值。rejected:失败,并保存一个原因。
一旦从 pending 变成 fulfilled 或 rejected,就不能再次改变:
1 | const promise = new Promise((resolve, reject) => { |
第一次确定结果后,后续 resolve 或 reject 不再改变这个 Promise。
这里经常说 Promise 被 settled,表示它已经 fulfilled 或 rejected。
3. executor 是同步执行的
new Promise(executor) 中的 executor 会立刻执行:
1 | console.log("A"); |
输出:
1 | A |
真正进入微任务的是 then 回调:
1 | console.log("A"); |
输出:
1 | A |
所以需要分清:
1 | 创建 Promise、执行 executor |
Promise 本身不负责把网络请求变成异步。真正的异步能力来自浏览器、Node.js、Cocos 或 native 宿主;Promise 负责统一表示结果。
4. resolve 不一定等于立即 fulfilled
传入普通值时:
1 | resolve(123); |
Promise 会以 123 fulfilled。
但传入另一个 Promise 或 thenable 时,外层 Promise 会采用它的最终状态:
1 | const inner = new Promise<string>(resolve => { |
outer 不会以 inner 这个对象作为普通成功值立刻结束,而是等待 inner:
1 | inner fulfilled("done") |
这类行为叫 Promise resolution procedure。它让不同来源的 Promise 可以自然地拼接成一条链。
5. then 总是返回一个新 Promise
1 | const p1 = Promise.resolve(1); |
之所以返回新 Promise,是因为回调执行后可能产生新的未来结果:
1 | const userPromise = getToken() |
每个 then 都代表链上的下一阶段。
回调结果决定新 Promise 的状态。
返回普通值
1 | Promise.resolve(1) |
返回普通值会让下一个 Promise fulfilled。
返回 Promise
1 | getToken() |
链会等待返回的 Promise,不会得到一个嵌套的 Promise<Promise<User>>。
抛出异常
1 | Promise.resolve("bad json") |
回调中抛出的异常会让 then 返回的新 Promise rejected。
什么都不返回
1 | Promise.resolve(1) |
函数没有显式返回值,相当于返回 undefined。
记忆锚点:
1 | then 回调返回普通值 |
6. 值和错误都会穿透
如果 then 没有成功处理函数,成功值会继续向后传:
1 | Promise.resolve(1) |
如果链中暂时没有失败处理函数,错误也会继续向后传:
1 | Promise.reject(new Error("failed")) |
因此一段异步流程可以把错误集中交给后面的 catch。
但错误被 catch 处理后,如果正常返回,链会恢复为 fulfilled:
1 | Promise.reject(new Error("failed")) |
如果不能真正恢复,应继续抛出:
1 | .catch(error => { |
7. catch 和 then 的关系
下面两种写法接近:
1 | promise.catch(onRejected); |
1 | promise.then(undefined, onRejected); |
但这两种写法有重要差异:
1 | promise.then(onFulfilled, onRejected); |
这里的 onRejected 处理的是 promise 原本的失败。它不能捕获同一个 then 中 onFulfilled 新抛出的异常,因为那个异常属于 then 返回的新 Promise。
更稳妥的链式写法是:
1 | promise |
这样 catch 能处理原 Promise 的失败,也能处理 onFulfilled 中的新异常。
8. finally 不处理结果,只处理收尾
Promise 的 finally 无论成功失败都会执行:
1 | showLoading(); |
finally 不接收成功值或失败原因:
1 | promise.finally(() => { |
正常返回时,它通常不会替换原结果:
1 | Promise.resolve(1) |
但如果 finally 抛错或返回 rejected Promise,会用新错误覆盖原结果:
1 | Promise.resolve(1) |
因此 finally 适合释放临时锁、关闭 loading、归还临时资源,不应该随意改变主流程。
9. throw 是异常流的入口
catch 只有遇到被抛出的异常才会执行,但这个 throw 不一定由业务代码显式写出。
显式抛出:
1 | throw new Error("配置不存在"); |
引擎或 API 内部抛出:
1 | JSON.parse("bad json"); // SyntaxError |
它们都会产生异常完成,运行时沿当前调用栈寻找最近的 catch。
1 | function parseConfig(text: string) { |
传播过程:
1 | JSON.parse 抛错 |
没有找到 catch 时,异常会交给当前宿主环境的未捕获错误处理机制。
10. return Error 不等于 throw Error
返回一个 Error 对象仍然是正常返回:
1 | function loadConfig() { |
抛出才会进入异常流:
1 | function loadConfig() { |
同样,返回一个失败状态也不会触发 catch:
1 | return { |
这两种错误表达都可以使用,但调用约定不同:
1 | 返回 Result |
不要返回一个 Error 对象,却又期待调用方自动进入 catch。
11. try/catch 只覆盖当前调用链
同步调用可以被外层捕获:
1 | try { |
定时器回调之后在新的任务中执行:
1 | try { |
执行顺序是:
1 | 注册 timer |
事件回调也是同样的边界:
1 | try { |
需要在回调内部处理,或者让回调返回的 Promise 被明确观察。
12. Promise rejection 不是外层同步 catch
Promise executor 中抛出的异常会被转换为 rejection:
1 | const promise = new Promise(() => { |
但下面的同步 try/catch 捕获不到:
1 | try { |
因为创建 rejected Promise 本身是正常返回了一个 Promise 对象。失败状态需要通过 Promise 链观察:
1 | Promise.reject(new Error("failed")) |
或者通过 await:
1 | try { |
await 会在当前 async 函数中把 rejection 表现为一次抛出,因此附近的 catch 可以捕获。
13. async 函数的错误一定变成 rejected Promise
async 函数总是返回 Promise:
1 | async function getValue() { |
等价结果是 fulfilled Promise:
1 | getValue().then(value => console.log(value)); // 1 |
async 函数内部抛错:
1 | async function getValue() { |
调用它不会同步向调用方抛出,而是返回 rejected Promise:
1 | const promise = getValue(); |
因此 Cocos 事件系统如果不关心回调返回值,这种写法可能留下未处理 rejection:
1 | private async onClick() { |
更明确的边界是:
1 | private onClick() { |
或者让 onClick 自己在内部 try/catch。
14. 只处理能够处理的错误
空 catch 会让真实问题消失:
1 | try { |
更合理的处理分为三类:
1 | 能够恢复 |
例如:
1 | try { |
包装错误时可以保留原始原因:
1 | try { |
错误信息应该补充上下文,但不要泄露 token、密码、完整请求头等敏感内容。
15. 自定义 Error 表达可识别的失败
1 | class ConfigError extends Error { |
调用方可以按错误类型决定是否恢复:
1 | try { |
不要只依赖错误文案字符串进行分支:
1 | if (error.message === "配置失败") { |
16. 四种 Promise 组合
Promise.all:全部成功,任一失败就失败
1 | const [user, inventory] = await Promise.all([ |
适合多个结果都必需的场景。
其中一个 rejected 后,Promise.all 会尽快 rejected,但其他底层任务不会自动取消。
Promise.allSettled:等待全部结束
1 | const results = await Promise.allSettled([ |
每项结果分别包含 fulfilled 或 rejected 状态。适合允许部分失败的批量任务。
Promise.race:第一个结束的结果
1 | const result = await Promise.race([ |
第一个 settled 的 Promise 决定结果,不论成功还是失败。输掉竞赛的任务同样不会自动取消。
Promise.any:第一个成功的结果
1 | const data = await Promise.any([ |
只要有一个 fulfilled 就成功;全部失败时以 AggregateError rejected。
17. Promise 没有通用的自动取消
丢弃 Promise 引用不等于取消底层任务:
1 | let promise = requestData(); |
网络、定时器或 native 工作可能仍然继续。
取消必须由底层能力配合:
1 | const controller = new AbortController(); |
不支持真正取消时,可以让结果失效:
1 | const requestId = ++this.requestId; |
这能阻止旧结果更新页面,但不代表底层请求没有继续消耗资源。
18. 常见 Promise 陷阱
忘记 return
1 | loadToken() |
应该返回:
1 | return loadUser(token); |
使用 new Promise(async executor)
1 | new Promise(async (resolve, reject) => { |
通常没有必要,而且 async executor 的错误边界容易变得混乱。已有 Promise 时直接返回:
1 | async function run() { |
甚至:
1 | function run() { |
没有观察 rejected Promise
1 | void submit(); |
如果 submit() 失败,可能产生未处理 rejection。应明确选择:
1 | await submit(); |
或者:
1 | void submit().catch(reportError); |
逻辑闭环 / 锚点
Promise 链本质上是在传递两条通道:
1 | 成功值 |
async/await 没有改变底层模型:
1 | await Promise |
最重要的判断顺序是:
1 | 这个函数返回普通值还是 Promise? |
关联
- 事件循环 vs 游戏帧循环:Promise 回调进入微任务,但不代表等待一帧。
- 内存与生命周期:等待中的异步流程如何保存回调和对象引用。
- 语言语义:普通返回、
throw、this和闭包是 Promise 链继续工作的语言基础。
来源:与 Codex 的对话,2026-07。