Fundamentals 16 min read

Step‑by‑Step Guide to Implementing a Custom Promise in JavaScript

This article provides a comprehensive, step‑by‑step tutorial on hand‑coding a JavaScript Promise class, covering its initial structure, state management, then method, error handling, asynchronous execution, callback queuing, and chainable behavior with complete code examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Step‑by‑Step Guide to Implementing a Custom Promise in JavaScript

Introduction

In this tutorial we will build a hand‑written Promise implementation from scratch. The guide is suitable for readers who are unfamiliar with native JavaScript promises or who want to understand the internal mechanics in depth.

1. Initial Structure – Creating the Class

We start by defining a MyPromise class and creating an instance with new Promise((resolve, reject) => {}) . The constructor receives an executor function that will be called immediately.

let promise = new Promise((resolve, reject) => {});

Inside the class we store the executor and later replace the native Promise with our own class:

class MyPromise {
    constructor(executor) {
        const resolve = (value) => {};
        const reject = (reason) => {};
        executor(resolve, reject);
    }
}

2. Defining State and Core Methods

A promise has three possible states: pending , fulfilled , and rejected . We define them as static properties and add instance fields for the current state, the resolved value, and the rejection reason.

class MyPromise {
    static PENDING = 'pending';
    static FULFILLED = 'fulfilled';
    static REJECTED = 'rejected';
    constructor(executor) {
        this.state = MyPromise.PENDING;
        this.value = undefined;
        this.reason = undefined;
        const resolve = (value) => {
            if (this.state === MyPromise.PENDING) {
                this.state = MyPromise.FULFILLED;
                this.value = value;
            }
        };
        const reject = (reason) => {
            if (this.state === MyPromise.PENDING) {
                this.state = MyPromise.REJECTED;
                this.reason = reason;
            }
        };
        executor(resolve, reject);
    }
}

3. Implementing then

The then method receives two callbacks – one for fulfillment and one for rejection. If the promise is already settled, the appropriate callback is executed asynchronously via setTimeout . If the promise is still pending, the callbacks are stored for later execution.

then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r };
    if (this.state === MyPromise.FULFILLED) {
        setTimeout(() => onFulfilled(this.value));
    }
    if (this.state === MyPromise.REJECTED) {
        setTimeout(() => onRejected(this.reason));
    }
    if (this.state === MyPromise.PENDING) {
        this.onFulfilledCallbacks.push(onFulfilled);
        this.onRejectedCallbacks.push(onRejected);
    }
}

4. Error Handling

To mimic native behavior, the constructor wraps the executor call in a try…catch block. If the executor throws, reject is called with the error.

try {
    executor(resolve, reject);
} catch (error) {
    reject(error);
}

5. Asynchronous Execution

Native promises resolve in a micro‑task queue. For simplicity we simulate asynchrony with setTimeout inside then and when invoking stored callbacks.

6. Callback Queuing

When the promise is still pending, callbacks are pushed into onFulfilledCallbacks and onRejectedCallbacks . Once resolve or reject runs, each stored callback is called in order.

this.onFulfilledCallbacks.forEach(cb => cb(this.value));
this.onRejectedCallbacks.forEach(cb => cb(this.reason));

7. Chainable then

To support chaining, then returns a new MyPromise . The result of the callback (or any thrown error) determines how the new promise settles.

return new MyPromise((resolve, reject) => {
    if (this.state === MyPromise.FULFILLED) {
        setTimeout(() => {
            try {
                const result = onFulfilled(this.value);
                resolve(result);
            } catch (e) {
                reject(e);
            }
        });
    }
    // similar handling for REJECTED and PENDING cases
});

Conclusion

The final implementation combines static state constants, proper error handling, asynchronous callback execution, callback storage for pending promises, and a chainable then method. With this custom MyPromise you can replicate the core behavior of native JavaScript promises.

JavaScriptprogrammingTutorialasyncPromise
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.