skip to content

How does Promise.resolve(value) differ from new Promise(resolve => resolve(value)), and what does Promise.resolve return when the argument is already a native promise?

level: middleimportance: should knowfreq 44%

answer

  1. one of them can hand back the same object
  2. identity check on the argument
  3. constructor must match for the short-circuit
  4. reject stores its argument verbatim

basics

~20 s

Promise.resolve is a shortcut for a promise that is already settled, with one extra rule: given a native promise it returns that exact object instead of wrapping it. The constructor form always allocates a new promise. Promise.reject never unwraps anything.

solid answer

~40 s

Both produce a promise that adopts `value`, but they differ in identity. `Promise.resolve(v)` first checks whether `v` is already a promise whose `constructor` is `Promise`; if so it returns *that very object*, so `Promise.resolve(p) === p` is `true`. The constructor form always allocates a fresh promise, so `new Promise(r => r(p)) === p` is `false` — the new promise merely tracks `p` and settles when it does. In practice `Promise.resolve` is how you normalise a value that might or might not be a promise, and how you get into a promise chain from a plain value. Its counterpart `Promise.reject(reason)` has no such short-circuit and no unwrapping at all: it always returns a new promise rejected *with* the argument, even if that argument is itself a promise.

code

javascript · 8 lines
javascript
const inner = Promise.resolve(1);
console.log(Promise.resolve(inner) === inner); // true - same object handed back
console.log(new Promise((resolve) => resolve(inner)) === inner); // false - new wrapper

const wrapped = Promise.reject(new Error('x'));
const outer = Promise.reject(wrapped); // rejected WITH the promise, no unwrapping
outer.catch((reason) => console.log(reason === wrapped)); // true
wrapped.catch(() => {});

go deeper

for a junior

Know that Promise.resolve(v) and Promise.reject(e) are the quick way to get an already-settled promise, without writing a constructor and executor.

for a middle

Explain the identity short-circuit — Promise.resolve returns a native promise argument unchanged — and that Promise.reject has no equivalent and never unwraps.

for a senior

Use Promise.resolve as the normalisation point in code that accepts sync-or-async handlers, and be able to say what it still does not catch: a synchronous throw at the call site.

for a principal

Weigh whether an API should accept sync-or-async handlers at all: normalising is cheap, but it hides latency differences and makes error timing inconsistent across implementations.

## Two ways to make an already-decided promise The constructor exists for wrapping something that is not yet promise-shaped — a callback API, a timer, an event. When you already have the value, the static methods are the direct route: ```js const a = Promise.resolve(1); const b = new Promise((resolve) => resolve(1)); // both fulfilled with 1 ``` For a plain value the two are equivalent in behaviour, and the static form is simply clearer and cheaper: no executor closure, no ceremony. ## The identity short-circuit The difference appears when the argument is itself a promise. `Promise.resolve` performs an identity check: if the argument is a promise *and* its `constructor` property is the same constructor `Promise.resolve` was called on, it hands the argument straight back. ```js const inner = Promise.resolve(1); Promise.resolve(inner) === inner; // true new Promise((resolve) => resolve(inner)) === inner; // false ``` In the constructor case a genuinely new promise object is created; calling `resolve(inner)` makes that new promise *track* `inner`, so it settles with the same outcome, but it is a distinct object and settles a turn later than `inner` does. The short-circuit is not merely an optimisation you can ignore. It means `Promise.resolve` is idempotent — feeding a promise through it repeatedly costs nothing and never adds layers. ## Why the check is constructor-sensitive The rule is deliberately conservative: the argument is returned as-is only when its `constructor` matches. A subclass instance passed to `Promise.resolve` is *not* returned unchanged, because the caller asked for a base `Promise` and getting back a subclass could carry different behaviour: ```js class MyPromise extends Promise {} const m = MyPromise.resolve(1); Promise.resolve(m) === m; // false — constructors differ MyPromise.resolve(m) === m; // true — same constructor ``` ## The normalisation idiom The most common real use is smoothing over an API that may return either a value or a promise: ```js function run(handler, input) { return Promise.resolve(handler(input)).then((out) => log(out)); } ``` If `handler` is synchronous you still get a promise; if it is asynchronous you get its promise back untouched, with no extra wrapper. This is how plugin and middleware layers accept both styles without branching on the return type. A related idiom is starting a chain from a constant, so that everything downstream is uniform: `Promise.resolve(seed).then(step1).then(step2)`. One caveat: `Promise.resolve(handler(input))` does not protect you from a *synchronous* throw inside `handler` — the call happens before `Promise.resolve` is ever reached, so the exception escapes as an ordinary exception rather than a rejection. ## Promise.reject is not symmetric It is tempting to assume `Promise.reject` mirrors `Promise.resolve`. It does not: ```js const wrapped = Promise.reject(new Error('x')); const outer = Promise.reject(wrapped); // outer is rejected WITH the promise object, not with the Error ``` There is no identity check and no unwrapping: whatever you pass becomes the rejection reason verbatim. That asymmetry is intentional. Resolution has to inspect its argument anyway, because resolving with a thenable means adopting its outcome; rejection has no such concept, so the reason is stored as an opaque value. The practical rule is to always pass `Promise.reject` an `Error` — passing a promise, a string, or a plain object produces a reason that is awkward to handle and loses the stack trace. A second practical note: `Promise.reject(...)` creates an already-rejected promise, and if you build one and do not attach a handler promptly, the runtime reports it as an unhandled rejection. ## Choosing between them Use the constructor only when you are genuinely bridging something non-promise into promise-land, and there is a callback or event whose completion you must translate into a `resolve`/`reject` call. Everything else — a known value, a value that might already be a promise, an immediate failure — is better expressed with `Promise.resolve` or `Promise.reject`. Reaching for `new Promise` around code that already returns a promise is a well-known anti-pattern: it adds an object, an extra settlement hop, and a chance to lose an error by forgetting to wire the rejection path.

  • Why does Promise.resolve check the argument's constructor rather than just whether it is a promise?
    Because subclasses can change behaviour. If you call `Promise.resolve(x)` you are asking for a base `Promise`, so returning an instance of a subclass unchanged could hand you an object with different semantics. The short-circuit therefore fires only when the argument's `constructor` is exactly the constructor the method was invoked on.
  • Is wrapping a function that already returns a promise in new Promise ever justified?
    Almost never — it is a classic anti-pattern. You gain an extra object and an extra settlement hop, and it is easy to wire `resolve` while forgetting `reject`, silently swallowing failures. The constructor is for bridging genuinely non-promise sources: callbacks, events, timers.
  • What is Promise.resolve(handler(x)) unable to protect you from?
    A synchronous throw inside `handler`. The call is evaluated before `Promise.resolve` runs, so the exception propagates as an ordinary exception at the call site rather than becoming a rejection. If you need every failure mode as a rejection, invoke the handler inside a promise-returning wrapper such as an async function.

saying these in an interview costs you the question

  • Says Promise.resolve always creates a brand-new promise object
  • Thinks Promise.reject unwraps a promise argument
  • Claims new Promise(r => r(p)) returns p itself
  • Wraps promise-returning functions in new Promise routinely
  • Passes non-Error values as the rejection reason

context