Implementing the Promises/A+ Specification from Scratch in JavaScript
A Promise implementation begins as a class. The constructor receives a callback, commonly referred to as the executor, which runs immediately with two functional parameters: resolve and reject.
class MyPromise {
constructor(executor) {
const resolve = () => {};
const reject = () => {};
executor(resolve, reject);
}
}
State Management and Core Rules
According to the specification, a promise must have three mutually exclusive states: pending, fulfilled, and rejected. When pending transitions to fulfilled, an immutable value is stored. On transition to rejected, an immutable reason is stored. Subsequent transitions are ignored. If the executor throws an error, reject is invoked automatically.
class MyPromise {
constructor(executor) {
this._state = 'pending';
this._value = undefined;
this._reason = undefined;
const resolve = (value) => {
if (this._state === 'pending') {
this._state = 'fulfilled';
this._value = value;
}
};
const reject = (reason) => {
if (this._state === 'pending') {
this._state = 'rejected';
this._reason = reason;
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
}
The then Method and Asynchronous Behavior
The then method registers onFulfilled and onRejected handlers. If the state is already settled, the matching handler is called synchronously. For pending promises, subscription lists store the callbacks so they can be invoked later, which enables asynchronous resolution.
class MyPromise {
constructor(executor) {
this._state = 'pending';
this._value = undefined;
this._reason = undefined;
this._onFulfilledCallbacks = [];
this._onRejectedCallbacks = [];
const resolve = (value) => {
if (this._state === 'pending') {
this._state = 'fulfilled';
this._value = value;
this._onFulfilledCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this._state === 'pending') {
this._state = 'rejected';
this._reason = reason;
this._onRejectedCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
if (this._state === 'fulfilled') {
onFulfilled(this._value);
} else if (this._state === 'rejected') {
onRejected(this._reason);
} else if (this._state === 'pending') {
this._onFulfilledCallbacks.push(() => onFulfilled(this._value));
this._onRejectedCallbacks.push(() => onRejected(this._reason));
}
}
}
Enabling Chaining with promise2
To support chaining like promise.then(...).then(...), the then method defines and returns a new MyPromise instance called promise2. Whatever the handler returns becomes the input x for the Promise Resolution Procedure.
then(onFulfilled, onRejected) {
const currentPromise = new MyPromise((resolve, reject) => {
if (this._state === 'fulfilled') {
const x = onFulfilled(this._value);
resolvePromise(currentPromise, x, resolve, reject);
} else if (this._state === 'rejected') {
const x = onRejected(this._reason);
resolvePromise(currentPromise, x, resolve, reject);
} else if (this._state === 'pending') {
this._onFulfilledCallbacks.push(() => {
const x = onFulfilled(this._value);
resolvePromise(currentPromise, x, resolve, reject);
});
this._onRejectedCallbacks.push(() => {
const x = onRejected(this._reason);
resolvePromise(currentPromise, x, resolve, reject);
});
}
});
return currentPromise;
}
The Promise Resolution Procedure
The recursive function resolvePromise handles the relationship between promise2 and x. Circular references cause a TypeError. If x is a thenable (an object or function with a then method), its then is called with resolve and reject callbacks, but only one settlement is allowed via a guard. Non-thenable values are passed to resolve directly.
function resolvePromise(targetPromise, x, resolve, reject) {
if (targetPromise === x) {
return reject(new TypeError('Chaining cycle detected'));
}
let settled = false;
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
(value) => {
if (settled) return;
settled = true;
resolvePromise(targetPromise, value, resolve, reject);
},
(error) => {
if (settled) return;
settled = true;
reject(error);
}
);
} else {
resolve(x);
}
} catch (error) {
if (settled) return;
settled = true;
reject(error);
}
} else {
resolve(x);
}
}
Handling Optional Handlers and Asynchronicity
If onFulfilled or onRejected are not functions, defaults propagate the value or the error. All handler invocations happen asynchronously via setTimeout. Errors thrown inside handlers are caught and forwarded to reject.
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === 'function'
? onRejected
: (error) => {
throw error;
};
const wrapperPromise = new MyPromise((resolve, reject) => {
const invokeFulfilled = () => {
setTimeout(() => {
try {
const x = onFulfilled(this._value);
resolvePromise(wrapperPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
};
const invokeRejected = () => {
setTimeout(() => {
try {
const x = onRejected(this._reason);
resolvePromise(wrapperPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
};
if (this._state === 'fulfilled') {
invokeFulfilled();
} else if (this._state === 'rejected') {
invokeRejected();
} else if (this._state === 'pending') {
this._onFulfilledCallbacks.push(invokeFulfilled);
this._onRejectedCallbacks.push(invokeRejected);
}
});
return wrapperPromise;
}
Static Helpers: resolve, reject, race, and all
Common utility methods wrap values, aggregate multiple promises, or return the first settled result.
MyPromise.resolve = function (value) {
return new MyPromise((resolve) => resolve(value));
};
MyPromise.reject = function (reason) {
return new MyPromise((_, reject) => reject(reason));
};
MyPromise.race = function (promises) {
return new MyPromise((resolve, reject) => {
promises.forEach((promise) => promise.then(resolve, reject));
});
};
MyPromise.all = function (promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let settledCount = 0;
function collect(index, value) {
results[index] = value;
settledCount += 1;
if (settledCount === promises.length) {
resolve(results);
}
}
promises.forEach((promise, index) => {
promise.then((value) => collect(index, value), reject);
});
});
};
Running the Compliance Test Suite
To verify correctness, add the following deferred utility and export the class. Then install and execute promises-aplus-tests.
MyPromise.defer = MyPromise.deferred = function () {
const deferred = {};
deferred.promise = new MyPromise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
};
module.exports = MyPromise;
Run the tests from the terminal:
npm install -g promises-aplus-tests
promises-aplus-tests your-file.js