Learn the concept
Promises & Async/Await
A Promise implementation requires managing three states (pending/fulfilled/rejected), storing callbacks via then(), executing them asynchronously when the state transitions, and supporting chaining by returning new Promises from then().
Implementing a Promise from scratch tests deep understanding of asynchronous JavaScript, the microtask queue, and callback chaining. A correct implementation must handle state transitions, asynchronous resolution, chaining, and error propagation.
A Promise has three states:
Once a Promise transitions from pending to fulfilled or rejected, it cannot change state again. This is a critical invariant.
then() is called before resolution, so callbacks must be stored and executed later.then() is called.then() returns a new Promise, enabling .then().then().catch() chains.then callback throws, the returned Promise is rejected with that error.then callback returns a Promise (thenable), the outer Promise adopts its state.The Promises/A+ spec defines the interoperability standard. Key rules:
then must return a PromiseonFulfilled or onRejected returns a value x, run the Promise Resolution Procedure on xonFulfilled or onRejected throws, the returned Promise must be rejectedonFulfilled and onRejected must be called asynchronously (after the execution context stack is empty)queueMicrotask() is the correct scheduling primitive. Using setTimeout works but doesn't match native Promise timing..then method), the Promise must recursively resolve it. This is what enables async/await interop.catch() is syntactic sugar for .then(undefined, onRejected).class MyPromise {
#state = 'pending';
#value = undefined;
#callbacks = [];
constructor(executor) {
const resolve = (value) => this.#transition('fulfilled', value);
const reject = (reason) => this.#transition('rejected', reason);
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
#transition(state, value) {
if (this.#state !== 'pending') return; // Can only transition once
this.#state = state;
this.#value = value;
this.#executeCallbacks();
}
#executeCallbacks() {
// Execute asynchronously via microtask
queueMicrotask(() => {
for (const cb of this.#callbacks) {
this.#handleCallback(cb);
}
this.#callbacks = [];
});
}
#handleCallback({ onFulfilled, onRejected, resolve, reject }) {
const handler = this.#state === 'fulfilled' ? onFulfilled : onRejected;
if (typeof handler !== 'function') {
// Pass through value/reason if no handler
(this.#state === 'fulfilled' ? resolve : reject)(this.#value);
return;
}
try {
const result = handler(this.#value);
// If result is a thenable, adopt its state
if (result && typeof result.then === 'function') {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const callback = { onFulfilled, onRejected, resolve, reject };
if (this.#state === 'pending') {
this.#callbacks.push(callback);
} else {
// Already settled — still execute asynchronously
this.#callbacks.push(callback);
this.#executeCallbacks();
}
});
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}Understanding Promise internals enables building custom async utilities like cancellable promises, retry logic with exponential backoff, and timeout wrappers.
Libraries and frameworks implement Promise-like abstractions for data fetching, state transitions, and animation sequencing.
Extend your custom Promise with static methods all(), race(), allSettled(), and any(). Write tests comparing behavior with native Promise.