Implement Promise from Scratch
Interview Question: "Implement a basic Promise that supports then, catch, and chaining."
Minimal Implementation
class MyPromise {
#state = 'pending';
#value = undefined;
#handlers = [];
constructor(executor) {
const resolve = (value) => {
if (this.#state !== 'pending') return;
if (value instanceof MyPromise) {
value.then(resolve, reject);
return;
}
this.#state = 'fulfilled';
this.#value = value;
this.#runHandlers();
};
const reject = (reason) => {
if (this.#state !== 'pending') return;
this.#state = 'rejected';
this.#value = reason;
this.#runHandlers();
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
#runHandlers() {
queueMicrotask(() => {
this.#handlers.forEach(({ onFulfilled, onRejected, resolve, reject }) => {
const handler = this.#state === 'fulfilled' ? onFulfilled : onRejected;
if (!handler) {
(this.#state === 'fulfilled' ? resolve : reject)(this.#value);
return;
}
try {
const result = handler(this.#value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (err) {
reject(err);
}
});
this.#handlers = [];
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#handlers.push({
onFulfilled: typeof onFulfilled === 'function' ? onFulfilled : null,
onRejected: typeof onRejected === 'function' ? onRejected : null,
resolve,
reject,
});
if (this.#state !== 'pending') {
this.#runHandlers();
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(onFinally) {
return this.then(
(value) => MyPromise.resolve(onFinally()).then(() => value),
(reason) => MyPromise.resolve(onFinally()).then(() => { throw reason; })
);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let remaining = 0;
for (const promise of promises) {
const index = remaining;
remaining++;
MyPromise.resolve(promise).then(
(value) => {
results[index] = value;
if (--remaining === 0) resolve(results);
},
reject
);
}
if (remaining === 0) resolve([]);
});
}
}Key Implementation Details
1. State Machine
A Promise is a state machine with three states and two transitions:
pending ββresolveβββ fulfilled
pending ββrejectββββ rejectedOnce settled, the state is immutable. This is why the guard if (this.#state !== 'pending') return exists.
2. Microtask Scheduling
queueMicrotask() ensures handlers run asynchronously, after the current synchronous code completes but before the next macrotask:
const p = new MyPromise(resolve => resolve(42));
p.then(v => console.log(v));
console.log('sync');
// Output: 'sync', 42 β handler runs after synchronous codeWithout queueMicrotask, synchronously resolved promises would call handlers synchronously, breaking the guarantee that .then callbacks always run asynchronously.
3. Chaining Mechanics
Each .then() returns a new promise. The resolution of that new promise depends on what the handler returns:
promise
.then(v => v + 1) // Return value β next promise resolves with it
.then(v => { throw 'err'; }) // Throw β next promise rejects
.then(v => new MyPromise(r => r(v))) // Return promise β adopt its state
.catch(err => 'recovered') // Catch returns β chain continues as fulfilled4. Value Propagation (Transparent Then)
If .then() is called without a handler for the current state, the value passes through:
MyPromise.reject('error')
.then(v => v) // No onRejected β error passes through
.then(v => v) // Still passes through
.catch(e => e); // Finally caught: 'error'This is why the if (!handler) branch forwards to resolve/reject directly.
5. Promise Resolution with Thenable
A spec-compliant implementation also handles "thenables" (objects with a .then method):
const resolve = (value) => {
if (value && typeof value.then === 'function') {
value.then(resolve, reject);
return;
}
// ... normal resolution
};Test Cases
// Basic resolution
new MyPromise(r => r(42)).then(v => console.log(v)); // 42
// Chaining
MyPromise.resolve(1)
.then(v => v + 1)
.then(v => v * 2)
.then(v => console.log(v)); // 4
// Error propagation
MyPromise.reject('fail')
.then(v => v)
.catch(e => console.log(e)); // 'fail'
// Async resolution
new MyPromise(r => setTimeout(() => r('delayed'), 100))
.then(v => console.log(v)); // 'delayed' (after 100ms)
// Promise.all
MyPromise.all([
MyPromise.resolve(1),
MyPromise.resolve(2),
MyPromise.resolve(3),
]).then(v => console.log(v)); // [1, 2, 3]
// finally
MyPromise.resolve('value')
.finally(() => console.log('cleanup'))
.then(v => console.log(v));
// 'cleanup', 'value'Common Mistakes
- Not using
queueMicrotaskβ handlers must always be async even for already-resolved promises - Not handling the case where
resolveis called with another Promise (recursive resolution) - Not guarding against double resolution (
resolvecalled twice) - Forgetting that
catchis justthen(null, onRejected) finallycallback's return value is ignored (original value passes through) unless it throws or returns a rejected promise