This post explains a quiz originally shared as a LinkedIn poll.
🔹 The Question
"use strict";
const promise = new Promise((resolve, reject) => {
setTimeout(() => reject("Rejected"), 0);
resolve("Resolved");
setTimeout(() => resolve("Timeout"), 0);
console.log("End");
});
promise
.then(data => console.log("then:", data))
.catch(err => console.log("catch:", err));
Hint: Once a promise changes state, can anything else change it again? Pay attention to which calls run synchronously inside the executor versus what gets scheduled.
Follow me for JavaScript puzzles and weekly curations of developer talks & insights at Talk::Overflow: https://talkoverflow.substack.com/
🔹 Solution
Correct answer: A) End, then: Resolved
The output is:
End
then: Resolved
🧠 How this works
This quiz tests one of the most fundamental — yet frequently misunderstood — rules of Promises: a promise can only be settled once. Once resolve() or reject() is called, the promise transitions from pending to either fulfilled or rejected, and that state is permanent. Any subsequent calls to resolve() or reject() are silently ignored.
Inside the Promise constructor, the executor function runs synchronously. The first setTimeout schedules a reject() as a macrotask (it doesn't run yet). Then resolve("Resolved") executes immediately and settles the promise as fulfilled. The second setTimeout schedules another resolve() as a macrotask (also doesn't run yet). Finally, console.log("End") prints.
When the macrotasks eventually fire, both the reject() and the second resolve() are no-ops — the promise is already settled.
This means only the .then() handler fires, and .catch() never executes.
🔍 Line-by-line explanation
new Promise((resolve, reject) => { ... })— The executor function runs synchronously the moment the Promise is constructed.setTimeout(() => reject("Rejected"), 0)— Schedules a callback on the macrotask queue. This does NOT run now. It will run after all synchronous code and microtasks complete.resolve("Resolved")— This runs synchronously and immediately settles the promise in thefulfilledstate with the value"Resolved". This is the critical moment — the promise is now permanently settled.setTimeout(() => resolve("Timeout"), 0)— Schedules another macrotask. Like the firstsetTimeout, this doesn't run yet.console.log("End")— Runs synchronously, printsEnd. Note: the executor continues running even afterresolve()is called —resolve()does not act likereturn.The executor finishes. The Promise constructor returns the (already fulfilled) promise to the variable
promise..then(data => console.log("then:", data))— Since the promise is already fulfilled, this handler is queued as a microtask..catch(err => console.log("catch:", err))— This handler is attached but will never fire because the promise is fulfilled, not rejected.Synchronous code is done. The microtask queue is processed: the
.then()handler runs and printsthen: Resolved.-
Macrotask queue is processed:
- First
setTimeoutfires:reject("Rejected")— silently ignored, promise is already settled. - Second
setTimeoutfires:resolve("Timeout")— silently ignored, promise is already settled.
- First
🔹 Key Takeaways
A promise can only be settled once. The first call to
resolve()orreject()wins. All subsequent calls are silently ignored — no errors, no warnings.resolve()does not stop executor execution. Unlikereturn, callingresolve()inside a Promise executor does not prevent the remaining code from running. If you want to stop, usereturn resolve(value).Synchronous code in the executor runs before any scheduled callbacks.
setTimeout(..., 0)schedules work for later (macrotask queue), whileresolve()called directly runs immediately during executor execution.The executor runs synchronously. The function passed to
new Promise()is invoked immediately and synchronously — it is not deferred or queued.When wrapping async operations in Promises, guard against double-settlement. Use a flag or
returnafter the firstresolve()/reject()call to make the intent explicit and prevent subtle bugs in multi-path async logic.
Top comments (0)