Frontend Development 32 min read

Implementing a Custom Promise in JavaScript: A Comprehensive Guide

This article provides a step‑by‑step tutorial on building a custom Promise implementation in JavaScript, covering basic functionality, handling of asynchronous logic, chaining, thenable objects, microtasks, error handling, static methods like resolve, reject, all, race, allSettled, any, and additional features such as catch and finally.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing a Custom Promise in JavaScript: A Comprehensive Guide

This article explains how to create a fully functional Promise class from scratch, starting with the basic states and constructor, then progressively adding features such as callback storage, asynchronous execution, chaining, and error handling.

Basic implementation defines the three states and the constructor that receives an executor:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class SelfPromise {
    status = PENDING;
    value = null;
    reason = null;
    constructor(executor) {
        try {
            executor(this.resolve, this.reject);
        } catch (error) {
            this.reject(error);
        }
    }
    resolve = (value) => {
        if (this.status === PENDING) {
            this.status = FULFILLED;
            this.value = value;
        }
    };
    reject = (reason) => {
        if (this.status === PENDING) {
            this.status = REJECTED;
            this.reason = reason;
        }
    };
}

To support asynchronous callbacks, the then method stores callbacks when the promise is pending and executes them later, using arrays to allow multiple then calls:

class SelfPromise {
    // ... previous code ...
    onFulfilledCallbacks = [];
    onRejectedCallbacks = [];
    then(onFulfilled, onRejected) {
        const promise2 = new SelfPromise((resolve, reject) => {
            const fulfilledMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const v = onFulfilled(this.value);
                        resolvePromise(promise2, v, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            };
            const rejectedMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const v = onRejected(this.reason);
                        resolvePromise(promise2, v, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                });
            };
            if (this.status === FULFILLED) {
                fulfilledMicrotask();
            } else if (this.status === REJECTED) {
                rejectedMicrotask();
            } else {
                this.onFulfilledCallbacks.push(fulfilledMicrotask);
                this.onRejectedCallbacks.push(rejectedMicrotask);
            }
        });
        return promise2;
    }
}

The helper resolvePromise handles returned values, including other promises, thenable objects, and prevents circular references:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected'));
    }
    if (x && (typeof x === "object" || typeof x === "function")) {
        let called = false;
        try {
            const then = x.then;
            if (typeof then === "function") {
                queueMicrotask(() => {
                    then.call(x,
                        (y) => { if (!called) { called = true; resolvePromise(promise2, y, resolve, reject); } },
                        (r) => { if (!called) { called = true; reject(r); } }
                    );
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (!called) { called = true; reject(e); }
        }
    } else {
        resolve(x);
    }
}

Static methods resolve and reject create already settled promises, handling thenables and non‑promise values:

static resolve(param) {
    if (param instanceof SelfPromise) return param;
    return new SelfPromise((resolve, reject) => {
        if (param && typeof param.then === "function") {
            queueMicrotask(() => param.then(resolve, reject));
        } else {
            resolve(param);
        }
    });
}

static reject(param) {
    return new SelfPromise((_, reject) => reject(param));
}

Utility methods catch and finally are thin wrappers around then , preserving value propagation and ensuring cleanup logic runs regardless of fulfillment or rejection:

catch(onRejected) {
    return this.then(null, onRejected);
}

finally(callback) {
    return this.then(
        (value) => SelfPromise.resolve(callback()).then(() => value),
        (reason) => SelfPromise.resolve(callback()).then(() => { throw reason; })
    );
}

Finally, the class implements the standard combinators all , race , allSettled , and any , each iterating over an iterable of promises, normalising values with SelfPromise.resolve , and aggregating results according to the semantics of the respective method.

programmingasynchronousPromiseCustomImplementation
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.