Promise.reject(reason) is the shortcut for "I already know this failed — hand me back a promise that's rejected and skip the executor." It's the mirror image of Promise.resolve, and you'll see it everywhere from input-validation guards to tests that need a pre-rejected promise to assert against.
Implement promiseReject(reason) as a standalone function. It returns a brand-new promise that is already in the rejected state, carrying reason as its rejection value. The reason is passed through verbatim — no wrapping, no conversion to Error, no special handling if it happens to be a promise.
function promiseReject(reason) {
// returns a Promise that is already rejected with `reason`.
}
promiseReject(new Error('boom')).catch((err) => {
console.log(err.message); // 'boom'
});
promiseReject('nope').catch((reason) => {
console.log(reason); // 'nope'
});
// The reason can be ANY value — not just Errors.
promiseReject(42).catch((r) => console.log(r)); // 42
promiseReject(undefined).catch((r) => console.log(r)); // undefined
// Passing a promise as the reason does NOT chain. The reason IS that promise.
const inner = Promise.resolve('hi');
promiseReject(inner).catch((r) => console.log(r === inner)); // true
.catch receives. Don't wrap strings in new Error(...), don't unwrap promises, don't coerce undefined to anything..catch handler still runs asynchronously (microtask), but the promise's internal state is settled before you return it.promiseReject(somePromise) rejects with somePromise itself as the reason, not with whatever that promise resolves or rejects to. This is the subtle difference from Promise.resolve, which does unwrap thenables.Promise constructor. The whole exercise is about wiring it up correctly with the executor function — don't reach for Promise.reject itself, that's cheating.You'll build a one-line wrapper around the Promise constructor that hands the executor's reject callback straight to the caller's reason. ("Executor" is the callback you pass to new Promise(...); the constructor calls it immediately with two arguments, resolve and reject.)
A promise is a placeholder for a value that doesn't exist yet. Most of the time you create one in the pending state — work happens in the background, then eventually it fulfills with a value or rejects with a reason. But sometimes you already know the work failed before you started: bad input, a missing token, a switch statement that hit a case you can't handle. promiseReject(reason) is the shortcut: hand back a promise that's already rejected, so the caller's .catch runs without you having to fake any asynchronous work.
A normal promise sits in pending until something inside the executor calls resolve(value) (moving it to fulfilled) or reject(reason) (moving it to rejected). promiseReject skips the waiting: the executor immediately calls reject(reason), so the promise is already in the rejected state by the time you return it. The caller never sees a pending step.
A reasonable first guess is to wrap the reason in new Error so consumers get something with a stack trace:
function promiseRejectBroken(reason) {
return new Promise((_, reject) => {
reject(new Error(reason)); // be helpful — always give back an Error
});
}
This feels friendly but it's wrong. The spec says Promise.reject(reason) rejects with reason exactly — same value, same reference. If the caller already passed new Error('boom'), this version double-wraps it: the .catch handler receives new Error('Error: boom') instead of the original error, losing the stack trace and the instanceof Error check breaks for callers comparing against the specific instance. If the caller passed a number or undefined, this coerces it to a string-based Error, which destroys information. Any code that does catch (err) { if (err === sentinel) ... } will silently mismatch.
function promiseReject(reason) {
// Build a new Promise. The constructor calls our executor synchronously,
// passing in (resolve, reject) — we only need reject.
// The first parameter is named `_` because we deliberately never call resolve.
return new Promise((_, reject) => {
// Pass `reason` straight through. No `new Error(reason)`, no `String(reason)`,
// no `if (reason == null)` guard. The spec requires verbatim pass-through —
// an Error stays the same instance, a string stays a string, undefined stays
// undefined, and a promise stays the promise itself (it is NOT unwrapped).
reject(reason);
});
}
module.exports = { promiseReject };
Two shifts from the naive version. First, we never touch reason — no wrapping, no coercion. Whatever the caller hands us is what .catch receives. Second, we use the standard Promise constructor with an executor that calls reject synchronously, which means the returned promise is already in the rejected state by the time return runs. The handler in .catch still fires asynchronously on the microtask queue — JavaScript's internal queue for scheduling promise callbacks, drained after the current synchronous code finishes but before any setTimeout or I/O — yet the promise's internal state is settled the moment you receive it. (So .catch waiting is a property of how promise handlers are scheduled, not a sign that the promise itself is still pending.)
Take promiseReject('boom').
promiseReject('boom'). Inside the function, reason is the string 'boom'.new Promise((_, reject) => reject('boom')). The Promise constructor calls the executor immediately and synchronously — this is a critical detail of the spec.reject('boom') runs. The promise's internal state flips from pending to rejected and its internal [[PromiseResult]] slot stores 'boom'..catch((r) => console.log(r)). The promise is already rejected, so JavaScript queues the catch handler on the microtask queue. As soon as the current synchronous code finishes, the runtime drains the microtask queue and runs the handler with r = 'boom'. The console prints boom.new Error — breaks reference equality and type checks. Concretely: a caller does const myErr = new MyCustomError('x'); promiseReject(myErr).catch(e => { if (e === myErr) handleSpecific(); else if (e instanceof MyCustomError) handleClass(); });. If your implementation does reject(new Error(reason)), both branches fail — e is a brand-new generic Error whose message is the stringified myErr ("Error: x"), so the identity check is false and instanceof MyCustomError is false. Fix: hand reason to reject exactly as you received it.Promise.reject directly — function promiseReject(r) { return Promise.reject(r); } produces the right runtime behavior, but the exercise exists to demonstrate that you understand the Promise constructor. The interviewer wants to see you write new Promise((_, reject) => reject(reason)) so they can confirm you know the executor runs synchronously and that reject is the second parameter. Fix: use new Promise((_, reject) => reject(reason)) even when one-liner alternatives exist.Promise.resolve is the famous case that does unwrap thenables (any object with a .then method, including real promises): Promise.resolve(Promise.resolve('hi')) waits and ultimately fulfills with 'hi'. promiseReject does the opposite: promiseReject(Promise.resolve('hi')) rejects with the promise object itself, not with 'hi'. If you wrote if (typeof reason?.then === 'function') reason.then(reject, reject) to "wait for it", you'd be reimplementing Promise.resolve's unwrapping behavior, which contradicts the spec for Promise.reject. Fix: never inspect reason; just pass it to reject.undefined — if (reason === undefined) reject(new Error('no reason')) corrupts a legal call. promiseReject() (no argument) must reject with undefined, just like the real Promise.reject(). Fix: no guard, no default — reject(reason) even when reason is undefined..catch to run synchronously — the promise's state is settled synchronously, but every .then/.catch callback runs as a microtask after the current synchronous code finishes. Concrete trap: let done = false; promiseReject('x').catch(() => { done = true; }); console.log(done); always logs false, because the .catch callback is queued on the microtask queue and only runs after console.log returns. Fix: use await or move the dependent code inside the .catch/.then callback.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Promise.reject(reason) is the shortcut for "I already know this failed — hand me back a promise that's rejected and skip the executor." It's the mirror image of Promise.resolve, and you'll see it everywhere from input-validation guards to tests that need a pre-rejected promise to assert against.
Implement promiseReject(reason) as a standalone function. It returns a brand-new promise that is already in the rejected state, carrying reason as its rejection value. The reason is passed through verbatim — no wrapping, no conversion to Error, no special handling if it happens to be a promise.
function promiseReject(reason) {
// returns a Promise that is already rejected with `reason`.
}
promiseReject(new Error('boom')).catch((err) => {
console.log(err.message); // 'boom'
});
promiseReject('nope').catch((reason) => {
console.log(reason); // 'nope'
});
// The reason can be ANY value — not just Errors.
promiseReject(42).catch((r) => console.log(r)); // 42
promiseReject(undefined).catch((r) => console.log(r)); // undefined
// Passing a promise as the reason does NOT chain. The reason IS that promise.
const inner = Promise.resolve('hi');
promiseReject(inner).catch((r) => console.log(r === inner)); // true
.catch receives. Don't wrap strings in new Error(...), don't unwrap promises, don't coerce undefined to anything..catch handler still runs asynchronously (microtask), but the promise's internal state is settled before you return it.promiseReject(somePromise) rejects with somePromise itself as the reason, not with whatever that promise resolves or rejects to. This is the subtle difference from Promise.resolve, which does unwrap thenables.Promise constructor. The whole exercise is about wiring it up correctly with the executor function — don't reach for Promise.reject itself, that's cheating.You'll build a one-line wrapper around the Promise constructor that hands the executor's reject callback straight to the caller's reason. ("Executor" is the callback you pass to new Promise(...); the constructor calls it immediately with two arguments, resolve and reject.)
A promise is a placeholder for a value that doesn't exist yet. Most of the time you create one in the pending state — work happens in the background, then eventually it fulfills with a value or rejects with a reason. But sometimes you already know the work failed before you started: bad input, a missing token, a switch statement that hit a case you can't handle. promiseReject(reason) is the shortcut: hand back a promise that's already rejected, so the caller's .catch runs without you having to fake any asynchronous work.
A normal promise sits in pending until something inside the executor calls resolve(value) (moving it to fulfilled) or reject(reason) (moving it to rejected). promiseReject skips the waiting: the executor immediately calls reject(reason), so the promise is already in the rejected state by the time you return it. The caller never sees a pending step.
A reasonable first guess is to wrap the reason in new Error so consumers get something with a stack trace:
function promiseRejectBroken(reason) {
return new Promise((_, reject) => {
reject(new Error(reason)); // be helpful — always give back an Error
});
}
This feels friendly but it's wrong. The spec says Promise.reject(reason) rejects with reason exactly — same value, same reference. If the caller already passed new Error('boom'), this version double-wraps it: the .catch handler receives new Error('Error: boom') instead of the original error, losing the stack trace and the instanceof Error check breaks for callers comparing against the specific instance. If the caller passed a number or undefined, this coerces it to a string-based Error, which destroys information. Any code that does catch (err) { if (err === sentinel) ... } will silently mismatch.
function promiseReject(reason) {
// Build a new Promise. The constructor calls our executor synchronously,
// passing in (resolve, reject) — we only need reject.
// The first parameter is named `_` because we deliberately never call resolve.
return new Promise((_, reject) => {
// Pass `reason` straight through. No `new Error(reason)`, no `String(reason)`,
// no `if (reason == null)` guard. The spec requires verbatim pass-through —
// an Error stays the same instance, a string stays a string, undefined stays
// undefined, and a promise stays the promise itself (it is NOT unwrapped).
reject(reason);
});
}
module.exports = { promiseReject };
Two shifts from the naive version. First, we never touch reason — no wrapping, no coercion. Whatever the caller hands us is what .catch receives. Second, we use the standard Promise constructor with an executor that calls reject synchronously, which means the returned promise is already in the rejected state by the time return runs. The handler in .catch still fires asynchronously on the microtask queue — JavaScript's internal queue for scheduling promise callbacks, drained after the current synchronous code finishes but before any setTimeout or I/O — yet the promise's internal state is settled the moment you receive it. (So .catch waiting is a property of how promise handlers are scheduled, not a sign that the promise itself is still pending.)
Take promiseReject('boom').
promiseReject('boom'). Inside the function, reason is the string 'boom'.new Promise((_, reject) => reject('boom')). The Promise constructor calls the executor immediately and synchronously — this is a critical detail of the spec.reject('boom') runs. The promise's internal state flips from pending to rejected and its internal [[PromiseResult]] slot stores 'boom'..catch((r) => console.log(r)). The promise is already rejected, so JavaScript queues the catch handler on the microtask queue. As soon as the current synchronous code finishes, the runtime drains the microtask queue and runs the handler with r = 'boom'. The console prints boom.new Error — breaks reference equality and type checks. Concretely: a caller does const myErr = new MyCustomError('x'); promiseReject(myErr).catch(e => { if (e === myErr) handleSpecific(); else if (e instanceof MyCustomError) handleClass(); });. If your implementation does reject(new Error(reason)), both branches fail — e is a brand-new generic Error whose message is the stringified myErr ("Error: x"), so the identity check is false and instanceof MyCustomError is false. Fix: hand reason to reject exactly as you received it.Promise.reject directly — function promiseReject(r) { return Promise.reject(r); } produces the right runtime behavior, but the exercise exists to demonstrate that you understand the Promise constructor. The interviewer wants to see you write new Promise((_, reject) => reject(reason)) so they can confirm you know the executor runs synchronously and that reject is the second parameter. Fix: use new Promise((_, reject) => reject(reason)) even when one-liner alternatives exist.Promise.resolve is the famous case that does unwrap thenables (any object with a .then method, including real promises): Promise.resolve(Promise.resolve('hi')) waits and ultimately fulfills with 'hi'. promiseReject does the opposite: promiseReject(Promise.resolve('hi')) rejects with the promise object itself, not with 'hi'. If you wrote if (typeof reason?.then === 'function') reason.then(reject, reject) to "wait for it", you'd be reimplementing Promise.resolve's unwrapping behavior, which contradicts the spec for Promise.reject. Fix: never inspect reason; just pass it to reject.undefined — if (reason === undefined) reject(new Error('no reason')) corrupts a legal call. promiseReject() (no argument) must reject with undefined, just like the real Promise.reject(). Fix: no guard, no default — reject(reason) even when reason is undefined..catch to run synchronously — the promise's state is settled synchronously, but every .then/.catch callback runs as a microtask after the current synchronous code finishes. Concrete trap: let done = false; promiseReject('x').catch(() => { done = true; }); console.log(done); always logs false, because the .catch callback is queued on the microtask queue and only runs after console.log returns. Fix: use await or move the dependent code inside the .catch/.then callback.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.