Promise 必知必会(十道题)
Category(分类): JavaScript Status: 已整理(2026)
本文保留原文的十道题和解题顺序,修正旧版 Node.js 的
UnhandledPromiseRejectionWarning、粘连输出、process.nextTick分类和环境依赖的运行结果,并补充 Promise 的状态、微任务、thenable、静态组合方法和错误处理边界。示例中涉及process的代码请使用 Node.js 运行;浏览器没有process.nextTick。
预备知识:状态、解决和兑现
Promise 常被概括为三种状态:
pending:等待中;fulfilled:已兑现,有结果值;rejected:已拒绝,有拒绝原因。
状态只能从 pending 变为 fulfilled 或 rejected,并且只能变化一次。规范中还使用“resolved(已解决)”描述 Promise 已经锁定到某个结果,包括它正在等待另一个 pending Promise 的情况,因此 resolved 不完全等同于 fulfilled:
const adopted = new Promise(resolve => {
resolve(new Promise(innerResolve => {
setTimeout(() => innerResolve('done'), 10)
}))
})
// adopted 已经采用了内层 Promise 的结果,但要等内层完成后才 fulfilled
adopted.then(console.log) // done
Promise 构造器中的 executor 会同步执行;then/catch 的处理函数会异步执行,通常排入 ECMAScript Promise job/microtask。
题目一:构造器同步,then 异步
const promise = new Promise(resolve => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
输出顺序:
1
2
4
3
解释:调用 new Promise 时 executor 立即运行;resolve() 只改变 Promise 状态,不会同步执行已经注册或随后注册的 then 处理函数。then 回调会在当前同步代码结束后执行。
题目二:then 返回新的 Promise
const promise1 = new Promise(resolve => {
setTimeout(() => resolve('success'), 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log(promise1)
console.log(promise2)
promise2.catch(error => {
console.log(error.message) // error!!!
})
setTimeout(() => {
console.log(promise1) // fulfilled with success
console.log(promise2) // rejected with Error
}, 2000)
刚创建时两个 Promise 通常都是 pending;约一秒后 promise1 fulfilled,promise2 因处理函数抛错而 rejected。重点是:
promise2不是promise1;- 每次调用
then都返回新的 Promise; - 处理函数抛出的异常会拒绝返回的 Promise;
- 应该对可能拒绝的 Promise 注册处理器。
原文记录的 UnhandledPromiseRejectionWarning 是旧版 Node.js 的输出格式。现代 Node.js 对未处理拒绝的默认行为和诊断文本可能随版本、启动参数变化,未处理 rejection 可能导致进程以非零状态退出。不要把警告文本当作 Promise 语义;及时 catch 才是正确做法。
题目三:第一次状态转换生效
const promise = new Promise((resolve, reject) => {
resolve('success1')
reject('error')
resolve('success2')
})
promise
.then(value => {
console.log('then:', value)
})
.catch(error => {
console.log('catch:', error)
})
输出:
then: success1
第一次调用 resolve 把 Promise 从 pending 变为 fulfilled,后续 reject 和 resolve 都不会改变它。这个“一次性”规则也适用于 thenable 解析过程中先调用的 resolve/reject。
题目四:每个 then 返回一环
Promise.resolve(1)
.then(value => {
console.log(value)
return 2
})
.catch(() => 3)
.then(value => {
console.log(value)
})
输出:
1
2
第一个 then 返回 2,所以它返回的 Promise fulfilled 为 2;由于没有发生 rejection,中间的 catch 不会执行,后一个 then 收到 2。
Promise 的链式调用不是通过 return this 实现的,而是每次创建新的 Promise 并把处理函数的结果解析到新 Promise 中。
题目五:同一个 Promise 可以注册多个 then
const promise = new Promise(resolve => {
setTimeout(() => {
console.log('once')
resolve('success')
}, 1000)
})
const start = Date.now()
promise.then(value => {
console.log('first:', value, Date.now() - start)
})
promise.then(value => {
console.log('second:', value, Date.now() - start)
})
典型输出类似:
once
first: success 1000
second: success 1000
实际毫秒数取决于计时器和系统调度,不能写死为 1005、1007。executor 只执行一次,但同一个 Promise 可以注册多个处理函数;这些处理函数按照注册顺序处理,且各自返回的 Promise 属于独立链。
题目六:返回 Error 对象不会自动拒绝
Promise.resolve()
.then(() => {
return new Error('error object')
})
.then(value => {
console.log('then:', value.message) // error object
})
.catch(error => {
console.log('catch:', error.message)
})
输出的是 then: error object。Error 只是一个普通对象,return 一个对象表示 fulfilled:
Promise.resolve()
.then(() => {
throw new Error('thrown error')
})
.catch(error => {
console.log('catch:', error.message) // thrown error
})
Promise.resolve()
.then(() => Promise.reject(new Error('rejected error')))
.catch(error => {
console.log('catch:', error.message) // rejected error
})
处理函数的返回值会按照 Promise.resolve 的规则解析:普通值(包括 Error 对象)fulfilled,抛出异常或返回 rejected Promise 才会进入后续拒绝路径。
题目七:返回自身会形成链式循环
let promise
promise = Promise.resolve().then(() => promise)
promise.catch(error => {
console.log(error.name) // TypeError
})
then 返回的 Promise 不能再等待自己,否则它永远无法完成。原生实现会检测这个直接自引用并拒绝,错误文本可能因 Node.js/浏览器而不同。
原文用无限递归的 process.nextTick 类比循环:
// 不要运行无限版本,它会持续占用 Node.js 的 nextTick 队列并阻塞 I/O。
let count = 0
function tick() {
if (count++ < 3) process.nextTick(tick)
}
process.nextTick(tick)
无限 nextTick 递归不是 Promise 自引用的实现机制,只能说明“持续排队可能让其他任务得不到机会”。两者都应避免混为一谈。
题目八:非函数处理器会值穿透
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(value => {
console.log(value)
})
输出:
1
then 的参数只有在是函数时才会被当作处理器:
- 成功处理器不是函数:使用
value => value; - 失败处理器不是函数:使用
reason => { throw reason }。
Promise.resolve(3) 这个表达式虽然会先创建一个 Promise,但它作为 then 参数不是函数,因此不会被执行,也不会改变值穿透结果。
题目九:then 的第二个参数捕获范围有限
Promise.resolve()
.then(
() => {
throw new Error('error')
},
error => {
console.log('fail1:', error)
}
)
.catch(error => {
console.log('fail2:', error.message)
})
输出:
fail2: error
then(onFulfilled, onRejected) 的两个处理器都只处理前一个 Promise的状态:
onRejected不能捕获同一次then的onFulfilled中抛出的异常;- 该异常会拒绝
then返回的新 Promise; - 后续的
catch可以捕获它。
这也是更常见地使用 .then(onFulfilled).catch(onRejected) 的原因:
fetch('/api/data')
.then(response => response.json())
.then(data => render(data))
.catch(error => {
console.error('请求或处理失败', error)
})
function render(data) {
console.log(data)
}
如果明确只想处理“上一步 Promise 的拒绝”,可以使用 then(undefined, onRejected),但它不会捕获同一个 then 的成功处理器抛错。
题目十:Node.js 中 nextTick、Promise job 与 setImmediate
// 请保存为 CommonJS 文件(例如 event-loop.cjs)运行
process.nextTick(() => {
console.log('nextTick')
})
Promise.resolve().then(() => {
console.log('then')
})
setImmediate(() => {
console.log('setImmediate')
})
console.log('end')
在常见的 Node.js CommonJS 启动场景中,通常看到:
end
nextTick
then
setImmediate
需要区分三个概念:
process.nextTick使用 Node.js 单独的 next tick 队列,通常在当前 JavaScript 调用栈结束后、继续事件循环前优先处理;Promise.then使用 ECMAScript Promise job/microtask 队列;setImmediate在 Node.js 事件循环的 check 阶段执行。
Node.js 的 CommonJS 与 ESM 启动路径、不同事件循环阶段、I/O 回调位置和版本都会影响具体顺序。尤其在 ESM 中,模块评估本身通过 Promise job 运行,Promise.then 与 process.nextTick 的相对观察顺序可能和 CommonJS 不同。不要把 nextTick 简单写成“和 Promise 完全相同的 microtask”。
setTimeout(..., 0) 与 setImmediate() 在主模块中谁先执行也不应写死;在 I/O 回调内部通常更容易观察到 setImmediate 先于计时器。需要验证顺序时,应注明 Node.js 版本、模块格式和代码所在事件循环阶段。
十一、Promise 静态方法补充
原文主要讨论 all 和 race,现代 Promise 还提供:
const tasks = [
Promise.resolve('a'),
Promise.reject(new Error('failed'))
]
Promise.all(tasks).catch(error => console.log(error.message)) // failed
Promise.allSettled(tasks).then(results => {
console.log(results.map(result => result.status)) // ['fulfilled', 'rejected']
})
Promise.any([
Promise.reject(new Error('first')),
Promise.resolve('success')
]).then(console.log) // success
Promise.race([
new Promise(resolve => setTimeout(() => resolve('slow'), 20)),
Promise.resolve('fast')
]).then(console.log) // fast
Promise.all:全部 fulfilled 才成功,任一 rejected 就拒绝,结果顺序与输入一致;Promise.allSettled:等待全部结束,返回每项状态;Promise.any:第一个 fulfilled 成功,全部 rejected 时以AggregateError拒绝;Promise.race:第一个 settled 决定结果,不会自动取消其他任务。
超时控制只让返回的 Promise 先失败,不会自动取消底层网络请求;网络请求需要配合 AbortController:
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, { signal: controller.signal })
return response
} finally {
clearTimeout(timer)
}
}
十二、常见结论
- executor 同步执行,Promise 处理器异步执行;
- Promise 状态只能从 pending 变化一次;
then/catch每次返回新 Promise;- 返回 Error 对象是 fulfilled,抛出 Error 或返回 rejected Promise 才是 rejected;
- 非函数处理器会发生值穿透;
- 同一个 Promise 可以有多个监听者,监听者之间不会互相等待;
then返回 thenable 时需要递归采用其结果;- Promise 自引用会导致 TypeError;
- Node.js
process.nextTick不是浏览器 Promise microtask 的同义词; race和超时 Promise 不会自动取消已经开始的任务;- 未处理 rejection 的输出和进程行为会随运行时版本变化,应显式处理错误。