Skip to content

Instantly share code, notes, and snippets.

@niquola
Last active January 20, 2026 12:15
Show Gist options
  • Select an option

  • Save niquola/5fc910b73a665c634a406a9851370faf to your computer and use it in GitHub Desktop.

Select an option

Save niquola/5fc910b73a665c634a406a9851370faf to your computer and use it in GitHub Desktop.
Implementing Promise from scratch in JavaScript
time ollama run glm-4.7-flash "implement promise funciton in js from scratch" > result.md
ollama run glm-4.7-flash "implement promise funciton in js from scratch" > 0.23s user 0.38s system 1% cpu 49.968 total

Thinking...

  1. Understand the Goal: The user wants to know how to implement a Promise function in JavaScript from scratch, without using native Promise.

  2. Target Audience: Likely a JavaScript developer (student or intermediate) looking to understand the underlying mechanics of Promises. The explanation should be clear, step-by-step, and use modern ES6+ syntax but avoid built-in Promise features.

  3. Key Concepts to Cover:

    • The state machine: pending -> fulfilled / rejected.
    • Callbacks: onFulfilled and onRejected.
    • Asynchronous execution: How to handle .then().
    • Handling multiple .then() calls (chaining).
    • The executor function (resolve, reject).
    • "Error Prone" scenarios: Passing non-functions to .then(), throwing errors in the executor, or calling resolve/reject multiple times.
  4. Step-by-Step Implementation Plan:

    • Step 1: The Class Structure. Create a class named MyPromise.
    • Step 2: Constructor & Initial State. Accept an executor. Set state to pending. Initialize empty arrays for onFulfilled and onRejected.
    • Step 3: The Resolve/Reject Methods. Define public methods to change state and trigger callbacks.
    • Step 4: Handling Asynchronous Callbacks. Use setTimeout or requestAnimationFrame to mimic microtasks/macrotasks so console.log shows the correct result (pending -> fulfilled).
    • Step 5: The .then() Method. Return a new MyPromise instance (chaining). Implement then logic: if state is fulfilled, call success callback; if rejected, call error callback.
    • Step 6: Handling Errors in then. If a user-provided callback throws an error, the new promise should be rejected.
    • Step 7: Final Polish. Add comments and a usage example.
  5. Drafting the Code (Mental or Scratchpad):

    class MyPromise {
      constructor(executor) {
        this.state = 'pending';
        this.value = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];
    
        const resolve = (val) => {
          // Change state if not fulfilled/rejected
          if (this.state !== 'pending') return;
          this.state = 'fulfilled';
          this.value = val;
          // Trigger callbacks
          this.onFulfilledCallbacks.forEach(fn => fn(this.value));
        };
    
        const reject = (val) => {
           // Similar logic for reject
        };
    
        executor(resolve, reject);
      }
    
      then(onFulfilled, onRejected) {
        // Need to return a new Promise to support chaining
        const promise2 = new MyPromise((resolve, reject) => {
           if (this.state === 'fulfilled') {
              // setTimeout needed to mimic async
              setTimeout(() => {
                 try {
                    let x = onFulfilled(this.value);
                    // handle x logic (return value or MyPromise)...
                    resolve(x);
                 } catch (err) {
                    reject(err);
                 }
              }, 0);
           } else if (this.state === 'rejected') {
              // Similar for reject
           } else {
              // Pending: push to queue
              this.onFulfilledCallbacks.push(() => {
                 setTimeout(() => {
                    try { resolve(onFulfilled(this.value)); } catch(e) { reject(e); }
                 }, 0);
              });
              this.onRejectedCallbacks.push(...);
           }
        });
        return promise2;
      }
    }
  6. Refining the Logic:

    • Crucial Detail: The Promise specification requires that if onFulfilled returns a promise (let's call it p2), the outer promise should resolve with the result of p2. For a basic implementation, resolving with the immediate value is usually enough, but handling return values (like 2 + 2) is important. Let's stick to a slightly simplified version first, or just handle basic return values. The core complexity is usually the state management and chaining. I will implement a version that handles basic chaining and return values.
  7. Structuring the Explanation:

    • Introduction: Briefly explain what a Promise is (state machine).
    • The Code: Provide the full code block.
    • Breakdown: Explain the constructor, resolve/reject, and then.
    • Advanced Features (Optional but good): Briefly mention handling "Resolution functions" (returning Promises) or multiple calls to resolve.
  8. Writing the Actual Response (Iterative refinement):

    • Code Draft:
      class MyPromise {
        constructor(executor) {
          this.state = 'pending';
          this.value = undefined;
          this.onResolvedCallbacks = [];
          this.onRejectedCallbacks = [];
      
          const resolve = (value) => {
            if (this.state !== 'pending') return;
            this.state = 'fulfilled';
            this.value = value;
            this.onResolvedCallbacks.forEach(fn => fn());
          };
      
          const reject = (reason) => {
             if (this.state !== 'pending') return;
             this.state = 'rejected';
             this.reason = reason;
             this.onRejectedCallbacks.forEach(fn => fn());
          };
      
          try {
             executor(resolve, reject);
          } catch (e) {
             reject(e);
          }
        }
      
        then(onFulfilled, onRejected) {
          const promise2 = new MyPromise((resolve, reject) => {
             // 1. Handle immediate resolution
             if (this.state === 'fulfilled') {
                setTimeout(() => {
                   try {
                      const x = onFulfilled(this.value);
                      resolvePromise(promise2, x, resolve, reject);
                   } catch (e) {
                      reject(e);
                   }
                }, 0);
             } else if (this.state === 'rejected') {
                setTimeout(() => {
                   try {
                      const x = onRejected(this.reason);
                      resolvePromise(promise2, x, resolve, reject);
                   } catch (e) {
                      reject(e);
                   }
                }, 0);
             } else {
                // 2. Handle pending state
                this.onResolvedCallbacks.push(() => {
                   setTimeout(() => {
                      try {
                         const x = onFulfilled(this.value);
                         resolvePromise(promise2, x, resolve, reject);
                      } catch (e) {
                         reject(e);
                      }
                   }, 0);
                });
                this.onRejectedCallbacks.push(() => {
                   setTimeout(() => {
                      try {
                         const x = onRejected(this.reason);
                         resolvePromise(promise2, x, resolve, reject);
                      } catch (e) {
                         reject(e);
                      }
                   }, 0);
                });
             }
          });
          return promise2;
        }
      }
      
      // Helper to handle return values
      function resolvePromise(promise, x, resolve, reject) {
         // (Standard Promise spec logic omitted for brevity, simplified to direct resolve)
         if (x === promise) return reject(new TypeError('Chaining cycle detected'));
         if (x instanceof MyPromise) x.then(resolve, reject);
         else resolve(x);
      }
    • Correction: The simplified resolvePromise is easier for a "from scratch" tutorial than the complex standard spec version. I will use the simplified version.
  9. Final Polish of the Output:

    • Make sure to use setTimeout inside .then to prove the asynchronous nature (checking console.log order).
    • Ensure comments explain why setTimeout is there.
  10. Final Output Generation: (This matches the provided good response).

    • Define the class.
    • Explain resolve and reject.
    • Explain then with queues.
    • Provide the full code snippet.
    • Provide usage examples.

(Self-Correction during drafting): I should make sure to handle the case where the executor throws an error. I'll wrap the executor() call in a try...catch.

Let's produce the solution. ...done thinking.

Implementing a Promise from scratch requires understanding three main concepts:

  1. State Management: The promise can only be in pending, fulfilled, or rejected.
  2. Queuing Callbacks: If the promise is pending, the callbacks passed to .then() must be stored and executed later.
  3. Chaining: The .then() method must return a new promise.

Here is a complete implementation of a MyPromise class.

The Code

class MyPromise {
  constructor(executor) {
    // 1. Initial state
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;

    // 2. Store callbacks for .then() calls
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    // 3. Define resolve and reject functions
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // Trigger all callbacks in the queue
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        // Trigger all callbacks in the queue
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    // 4. Execute the user provided function
    // Wrap in try/catch to handle sync errors in executor
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    // Always return a new Promise to support chaining
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        // We use setTimeout to mimic microtask/mactask behavior
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            // Simple resolution logic: if it's a promise, resolve its value, else just resolve
            if (x instanceof MyPromise) {
              x.then(resolve, reject);
            } else {
              resolve(x);
            }
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            if (x instanceof MyPromise) {
              x.then(resolve, reject);
            } else {
              resolve(x);
            }
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else if (this.state === 'pending') {
        // If still pending, push callbacks to the queue
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              if (x instanceof MyPromise) {
                x.then(resolve, reject);
              } else {
                resolve(x);
              }
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              if (x instanceof MyPromise) {
                x.then(resolve, reject);
              } else {
                resolve(x);
              }
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      }
    });
    return promise2;
  }
}

Explanation of Key Parts

1. The Constructor & State

this.state = 'pending';
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];

Promises are state machines. We start in the pending state. We also create empty arrays to store the functions provided by .then(). This is crucial because if you call .then() before the resolve function is actually executed (e.g., if the async operation is still running), those callbacks shouldn't be run yet.

2. resolve and reject

These are the public methods exposed to the executor. If the executor calls resolve, we change the state to fulfilled and iterate over the queue to run any waiting .then() callbacks.

3. then()

This is the most complex part because of chaining.

  • It must return a new MyPromise.
  • It must handle the state:
    • If Fulfilled: Call onFulfilled immediately (after a microtask simulation).
    • If Rejected: Call onRejected immediately.
    • If Pending: Push the callbacks into the queue defined in the constructor.

4. Handling Asynchronous Logic

Notice the setTimeout calls. Native Promises resolve immediately in the same microtask queue as console.log. To ensure your "from scratch" implementation mimics this behavior, you need to schedule the execution of callbacks after the current call stack clears.

Usage Example

const promise = new MyPromise((resolve, reject) => {
  console.log('1. Executor running');
  setTimeout(() => {
    console.log('2. Setting resolve');
    resolve('Hello from Promise!');
  }, 1000);
});

promise.then((val) => {
  console.log('3. .then received:', val);
  return 'Chain value'; // You can return values to chain
}).then((val) => {
  console.log('4. Second .then received:', val);
});

/*
Output:
1. Executor running
2. Setting resolve
3. .then received: Hello from Promise!
4. Second .then received: Chain value
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment