skip to content

Review this JavaScript, where db.query(id) already returns a promise: function getUser(id) { return new Promise((resolve, reject) => { db.query(id).then(row => resolve(row), err => reject(err)); }); } — what is wrong with it, and when is new Promise actually the right tool?

level: middleimportance: should knowfreq 55%

answer

  1. the constructor is for creating, not forwarding
  2. executor already holds a promise
  3. a redundant layer that can lose errors
  4. forgotten reject path means a permanent hang
  5. just return the inner promise

basics

~20 s

This is the explicit promise construction antipattern: db.query already returns a promise, so the wrapper adds a layer that can only lose errors. Return db.query(id) directly. Reserve new Promise for adapting non-promise sources such as callbacks, events, or timers.

solid answer

~50 s

`db.query(id)` is already a promise, so the wrapper is pure overhead with a bug surface. The correct code is `return db.query(id)`, or an `async` function if there is more to do. What the wrapper costs you is real: any throw inside the `row => resolve(row)` handler happens after the promise has settled, so it is swallowed entirely; if someone later drops the rejection handler, the outer promise stays pending forever while the inner rejection goes unhandled; and every rewrite adds another `.then` layer for no gain. `new Promise` earns its place only when you are *creating* a promise from something that is not one — an error-first callback, a one-shot event, a timer, or a deferred you settle from elsewhere. If the executor body contains `.then`, `await`, or the word `async`, that is the review smell.

code

javascript · 19 lines
javascript
const db = { query: (id) => Promise.resolve({ id, name: 'ada' }) };

// antipattern: wrapping a promise in a promise
function getUserBad(id) {
  return new Promise((resolve) => {
    db.query(id).then((row) => {
      resolve(row);
      throw new Error('lost'); // settled already: nobody ever sees this
    });
  });
}

// correct: adopt the existing promise
function getUser(id) {
  return db.query(id);
}

getUserBad(1).then((r) => console.log('bad path ok:', r.name));
getUser(1).then((r) => console.log('good path ok:', r.name));

go deeper

for a junior

Recognise the shape: a new Promise whose body immediately calls .then on another promise. Say that the inner promise should just be returned, and name the pattern as the explicit promise construction antipattern.

for a middle

Explain the concrete losses — a throw after resolve is swallowed, a missing reject path turns an error into a permanent hang — and state the rule that returning a promise from then or from an async function adopts it.

for a senior

Show the async-executor variant producing an unhandled rejection and a stalled caller at the same time, and describe the review heuristic: every new Promise must name the non-promise mechanism it adapts.

for a principal

Frame it as an API-boundary standard: library code should hand back the driver's own promise rather than a re-wrapped copy, so instrumentation, subclassing, and rejection reasons survive across layers.

## The name and the shape This is usually called the explicit promise construction antipattern (or the deferred antipattern). Its signature is a `new Promise` whose executor immediately consumes another promise and forwards its settlement. The give-away in review is simple: **if the executor body contains `.then`, `await`, or `async`, you already had a promise and did not need the constructor.** ## Why it looks harmless The code appears to work. `db.query(id)` settles, one of the two handlers runs, and the outer promise settles with the same value or reason. Callers cannot tell the difference in the happy path, which is exactly why the pattern survives code review and spreads. ## What it actually costs **1. It swallows throws.** Once `resolve(row)` has been called, the promise is settled and immutable. Any exception raised afterwards inside that handler has nowhere to go: it rejects the invisible promise created by `.then`, which nobody holds. ```js return new Promise((resolve, reject) => { db.query(id).then((row) => { resolve(row); audit(row); // if this throws, nothing observes it }, reject); }); ``` With `return db.query(id).then(...)` the same throw becomes an ordinary rejection your caller can `catch`. **2. It converts a rejection into a hang.** The rejection path is manual, so it can be forgotten. Write only `.then(row => resolve(row))` and a failing query leaves the outer promise pending *forever* — the caller awaits something that will never settle — while the inner rejection is reported separately as an unhandled rejection. The wrapper turned a clean error into a silent stall plus a confusing log line. **3. It loses identity and adds layers.** The returned promise is a plain `Promise`, not whatever `db.query` returned, so any subclass, extra methods, or instrumentation the driver attached are gone. Each layer also inserts extra microtask hops before the value reaches the caller. **4. It signals confusion.** A reviewer reads it as "the author does not trust promises to compose", and the same author usually also writes `new Promise(resolve => resolve(value))` instead of `Promise.resolve(value)`. ## The async-executor variant is worse ```js return new Promise(async (resolve) => { const row = await db.query(id); // if this rejects... resolve(row); }); ``` The `Promise` constructor ignores the executor's return value, so the promise returned by the `async` executor is discarded. A rejection inside it therefore has no handler at all, and because `resolve` was never reached, the outer promise stays pending. One line produces both an unhandled rejection and a permanent hang. ## The rewrite ```js function getUser(id) { return db.query(id); } // or, when there is genuinely more to do: async function getUser(id) { const row = await db.query(id); return toUser(row); } ``` Returning a promise from a `.then` handler or from an `async` function adopts it: the outer promise waits for the inner one and mirrors its settlement, including its rejection. That adoption is exactly what the hand-written wrapper was trying — badly — to reimplement. ## When new Promise is right Use the constructor when no promise exists yet and you must create one from a different mechanism: - an error-first callback API with no promise form; - a one-shot event or a `message`/`load`-style notification; - a timer: `new Promise(r => setTimeout(r, ms))`; - a **deferred**, where you deliberately capture `resolve` and `reject` and settle them from elsewhere — for example a request/response protocol over a socket, where a reply arrives later carrying a correlation id. ```js const pending = new Map(); function request(id, payload) { return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); socket.send({ id, payload }); }); } // elsewhere: pending.get(msg.id).resolve(msg.body) ``` That deferred is legitimate because there is no promise to adopt — the settlement genuinely arrives from another part of the program. Even then, keep the executor tiny and guarantee that every path settles exactly once. ## Spotting it in review Ask one question of every `new Promise`: *what non-promise thing is being adapted here?* If the answer names a callback, an event, or a timer, it is correct. If the answer is "another promise", delete the constructor and return the inner promise.

  • Is new Promise(resolve => resolve(value)) ever justified?
    No. `Promise.resolve(value)` does the same thing in one call, and inside an `async` function a plain `return value` is enough. The constructor form only adds a closure and an extra chance to forget the rejection path. Reach for `Promise.resolve` when you need a promise from a known value or want to normalise a possibly-thenable input.
  • What specifically breaks if the executor is an async function?
    The constructor discards the executor's return value, so the promise the async executor produces is unheld. A rejection inside it is reported as an unhandled rejection, and since `resolve` was never reached the outer promise stays pending forever. You get a silent hang plus a log line that points at the inner error rather than the stalled caller.
  • How do you decide, in review, whether a given new Promise is legitimate?
    Ask what non-promise mechanism it adapts. A callback, a one-shot event, a timer, or a settlement that genuinely arrives from another part of the program all justify it. If the honest answer is "another promise", delete the constructor and return the inner promise directly, letting adoption forward both the value and the rejection.

saying these in an interview costs you the question

  • Wrapping a promise-returning call in new Promise to be safe
  • Says the extra layer only costs a little performance
  • Uses an async function as the promise executor
  • Forgets the rejection path so callers hang forever
  • Thinks new Promise is required before you can await

context