skip to content

The executor function you pass to new Promise() runs immediately and synchronously. What practical consequences does that have, and how would you arrange for the work to start only when a caller actually asks for it?

level: middleimportance: should knowfreq 55%

answer

  1. construction is the commitment point
  2. the executor is not deferred
  3. a promise is running, not planned
  4. hand out a factory function for laziness

basics

~20 s

Constructing a promise runs the executor right away, so the work it kicks off is already in flight before anyone subscribes. Promises are eager, not lazy. To defer work, hand out a function that creates the promise instead of the promise itself.

solid answer

~50 s

`new Promise(executor)` calls the executor synchronously, inside the constructor call, before the constructor returns. So a promise is not a description of work to do later — by the time you hold it, the work has already begun. That has three practical consequences. First, any synchronous code in the executor blocks the caller just like ordinary code. Second, subscribing with `.then` or `await` does not start anything; it only registers interest in a result already on its way. Third, there is no way to cancel or delay it by not subscribing. If you want laziness — for a retry, a conditional fetch, or work that may never be needed — expose a *function* that returns a fresh promise on each call, and let the caller decide when to invoke it. That is also why every retry helper takes a factory function rather than a promise.

go deeper

for a junior

Remember that the function you pass to new Promise runs right away, at construction time — not later when someone awaits the promise.

for a middle

Explain the ordering it produces: executor code before the statement after the constructor, handlers after the synchronous run finishes. Then show the thunk pattern for deferring work.

for a senior

Point out where eagerness costs real money in production — speculatively constructed promises doing work nobody consumes — and where it earns it, by overlapping independent operations.

for a principal

Own the API-shape decision: exposing a promise commits every caller to work that is already running, while exposing a factory keeps scheduling, retry, and concurrency limits in the caller's hands.

## Eager by construction ```js const p = new Promise((resolve) => { console.log('executor running'); resolve(1); }); console.log('after constructor'); // executor running // after constructor ``` The `Promise` constructor invokes the executor immediately, synchronously, before it returns the promise object. Nothing about creating a promise is deferred: whatever the executor starts — a timer, a network request, a database query, a CPU-bound loop — starts at that moment. This makes a promise fundamentally different from a lazy task abstraction. In lazy models, the object you hold is a *recipe*, and the work begins when someone runs it. In JavaScript, the object you hold is a *result already in progress*. Subscribing later with `.then()` or `await` adds an observer; it never triggers anything. ## Consequence 1: synchronous work in an executor still blocks Wrapping code in `new Promise` does not move it off the main thread or defer it: ```js const slow = new Promise((resolve) => { let total = 0; for (let i = 0; i < 1e9; i++) total += i; // blocks right here resolve(total); }); ``` The loop runs to completion before the constructor returns. A common misconception is that the promise wrapper "makes it async". It does not — only genuinely asynchronous sources (timers, I/O, another job) create asynchrony. The promise merely delivers the outcome asynchronously. ## Consequence 2: creating is committing Because construction starts the work, code that builds promises "just in case" does the work whether or not it is needed: ```js const a = expensiveA(); // already running const b = expensiveB(); // already running const result = condition ? await a : await b; // too late — both ran ``` Both calls executed. Choosing which one to await afterwards changes only which result you look at. To make the choice actually skip the work, delay construction: ```js const result = condition ? await expensiveA() : await expensiveB(); ``` ## Consequence 3: not subscribing does not cancel Dropping a promise on the floor does not stop what it started; it only means nobody looks at the answer. A promise has no cancellation channel of its own — abandoning it leaves the underlying operation running, and if it rejects with no handler attached, the runtime reports an unhandled rejection. ## Making work lazy: hand out a function The language-level answer to "I want this to start later" is a *thunk* — a zero-argument function that produces a fresh promise per call: ```js const task = () => new Promise((resolve) => setTimeout(() => resolve('done'), 100)); // nothing has happened yet const first = task(); // starts now const second = task(); // an independent second run ``` This pattern falls out of the state machine as much as from eagerness: a promise settles once and keeps that outcome forever, so it can never represent a second attempt. Anything that needs to run the work more than once — a retry, a poll, a per-attempt timeout, a queue that paces execution — must hold the function, not the promise. That is exactly why such helpers have signatures like `retry(fn, options)` rather than `retry(promise, options)`. ## Eagerness is visible in ordering too Because the executor is synchronous, code inside it runs *before* anything you write after the constructor call, while the handlers you attach run later: ```js console.log('1'); new Promise((resolve) => { console.log('2'); resolve(); }) .then(() => console.log('4')); console.log('3'); // 1, 2, 3, 4 ``` The `2` prints synchronously; `4` prints after the current synchronous code finishes. ## When the eagerness is what you want Eagerness is not a defect. Starting several independent operations and awaiting them afterwards is precisely how you get concurrency instead of a sequence of round trips, and a promise you construct early is a promise whose latency overlaps with your other work. Sharing one in-flight promise between several callers is a legitimate deduplication technique for exactly this reason. The rule to internalise is simply that construction is the commitment point, so build the promise at the moment you actually want the work to begin — no earlier, and no later.

  • Does wrapping a slow synchronous loop in new Promise make it non-blocking?
    No. The executor runs synchronously, so the loop blocks exactly as it would unwrapped. The promise only changes how the *result* is delivered, not when the work happens. Making CPU work non-blocking requires actually breaking it up or moving it off the main thread — the promise wrapper alone achieves nothing.
  • Why do retry and timeout helpers take a function rather than a promise?
    Because a promise represents one already-started, already-settled-once attempt. Given a promise there is nothing to retry — resubscribing returns the same outcome. Taking a factory lets the helper call it again for each attempt, producing a genuinely new operation every time.
  • If I never attach a handler to a promise, does the work it started stop?
    No. The operation runs to completion regardless; you have only chosen not to observe the result. Promises have no built-in cancellation, so abandoning one wastes the work rather than stopping it — and if it rejects, the runtime will report an unhandled rejection.

saying these in an interview costs you the question

  • Thinks new Promise defers or backgrounds the executor code
  • Believes the work starts when you call .then or await
  • Says wrapping sync code in a promise makes it non-blocking
  • Claims dropping a promise cancels the underlying operation
  • Passes a promise, not a factory, to a retry helper

context