- Published on
Promise/A+ Analysis
- Authors

- Name
- 乘方
Promise/A+ Analysis
What You Should Understand About Promise
Promise has three states
pending,fulfilled,rejected- Once state changes from
pendingtofulfilled/rejected, it is settled and cannot change again executor(),resolve(),reject()run synchronously by default
const executor = (resolve, reject) => {
resolve("fulfilled"); // settle as fulfilled
reject("rejected"); // ignored after settlement
};
new Promise(executor);
reject() vs throw new Error() inside executor
- In executor's synchronous phase,
throw new Error()is captured by Promise internals (try...catch), then converted toreject(error). - So in sync code, both
reject(reason)andthrow new Error()move Promise torejected. - After
catchorthen(_, onRejected), returning a normal value switches downstream chain tofulfilled.
Promise.reject("initial error")
.catch((err) => {
console.log("Caught:", err);
return "recovered value"; // downstream becomes fulfilled
})
.then((value) => {
console.log("Next then gets:", value); // Next then gets: recovered value
return "keep going";
})
.catch(() => {
console.log("This catch will not run, error is already handled");
});
- But
throw new Error()inside an async callback is outside Promise constructor's sync context, so current Promise.catchusually cannot catch it.
// Async throw cannot be caught by this promise chain
new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error("async error"); // uncaught global error
}, 0);
}).catch((e) => console.log(e)); // won't run
// Correct: pass async errors to reject explicitly
new Promise((resolve, reject) => {
setTimeout(() => {
try {
throw new Error("async error");
} catch (e) {
reject(e);
}
}, 0);
}).catch((e) => console.log("Caught:", e.message));
When then(onFulfilled, onRejected) callbacks run
- Calling
p.then(...)itself is synchronous. - Promise
pstate determines whether and which reaction (onFulfilled/onRejected) runs.- If state is already settled,
thenschedules the matching reaction as a microtask (e.g.queueMicrotask) and returns a new Promise.queueMicrotaskpushes callbacks to microtask queue and is supported in browsers and Node.js. - If
pis still pending, reactions are stored first. Afterresolve/reject, they are scheduled as microtasks.
So callbacks are asynchronous even thoughthencall is synchronous.
- If state is already settled,
// Schedule into microtask queue
function asyncRun(fn) {
if (typeof queueMicrotask === "function") {
queueMicrotask(fn);
return;
}
setTimeout(fn, 0);
}
// In Promise.then()
const runFulfilled = () => {
asyncRun(() => {
try {
const x = realOnFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
if (this.state === FULFILLED) {
runFulfilled();
} else if (this.state === REJECTED) {
runRejected();
} else {
// pending: store now, trigger later
this.onFulfilledCallbacks.push(runFulfilled);
this.onRejectedCallbacks.push(runRejected);
}
What happens in Promise.then() chaining
Example:
const p2 = p1.then(fn1, fn2);
const p3 = p2.then(fn3, fn4);
Question: The chain shape is built during sync code, but what controls each callback execution?
A Promise state is determined by
resolve()/reject().p1state controlsfn1 / fn2, and returnsp2.Then
p2state controlsfn3 / fn4.
So the core is: how do we determinep2state?p2state is determined by return valuexof callback in previous.then.- If
xis not thenable,p2becomes fulfilled withx. - If
xis object/function, it may be thenable. - If
xis thenable, its own state machine controls outcome, andp2adopts it. - If outer Promise resolves with inner Promise, spec says outer Promise should follow inner final result (flatten), not pass Promise object as plain value.
const p = new Promise((resolve, reject) => { const value = Promise.reject("rejected"); resolve(value); }); p.then( (val) => { console.log("success", val); // won't run }, (reason) => { console.log("fail", reason); // fail rejected }, );- If
Double protection:
resolvefast-path for native Promise (value instanceof Promise) is an optimization.resolvePromiseviathen.callfor generic thenable is spec-critical.- Combining both gives performance + compatibility.
// resolve implementation
const resolve = (value) => {
if (value instanceof Promise) {
value.then(resolve, reject);
return;
}
// ...
};
// resolvePromise implementation
function resolvePromise(promise2, x, resolve, reject) {
// 1. Prevent self-resolution cycles
if (promise2 === x) {
reject(new TypeError("Chaining cycle detected for promise"));
return;
}
// 2. Only object/function can be thenable
if (x !== null && (typeof x === "object" || typeof x === "function")) {
let called = false;
try {
const then = x.then;
if (typeof then === "function") {
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
},
);
return;
}
} catch (error) {
if (called) return;
called = true;
reject(error);
return;
}
}
// 3. Plain value: fulfill directly
resolve(x);
}
Other Promise APIs
resolve
- If input is already a Promise, return it directly
- Otherwise wrap into fulfilled Promise
static resolve(value) {
if (value instanceof Promise) return value;
return new Promise((resolve) => resolve(value));
}
reject
- Static method on Promise
- Returns a rejected Promise directly
static reject(reason) {
return new Promise((_, reject) => reject(reason));
}
catch
- Sugar for
then(null, onRejected)
catch(onRejected) {
return this.then(null, onRejected);
}
finally
- Does not alter previous value/error (unless finally throws or returns rejected)
- Runs regardless of fulfilled/rejected
finally(onFinally) {
const handler =
typeof onFinally === "function" ? onFinally : () => undefined;
return this.then(
(value) => Promise.resolve(handler()).then(() => value),
(reason) =>
Promise.resolve(handler()).then(() => {
throw reason;
}),
);
}
all
- Fulfilled only when all are fulfilled (preserve input order)
- Reject immediately once any item rejects
static all(iterable) {
return new Promise((resolve, reject) => {
const items = Array.from(iterable);
if (items.length === 0) {
resolve([]);
return;
}
const result = new Array(items.length);
let count = 0;
items.forEach((item, index) => {
Promise.resolve(item).then(
(value) => {
result[index] = value;
count += 1;
if (count === items.length) resolve(result);
},
(reason) => reject(reason),
);
});
});
}
race
- Whichever settles first (
fulfilledorrejected) wins
static race(iterable) {
return new Promise((resolve, reject) => {
Array.from(iterable).forEach((item) => {
Promise.resolve(item).then(resolve, reject);
});
});
}