Building a Simple Promise Implementation in JavaScript
Tags: javascript, programming, webdev
Published: 2024-02-17 15:24:52

## Introduction

Promises are a fundamental concept in modern JavaScript, providing a clean and efficient way to handle asynchronous operations. This article will explore the inner workings of a simple Promise implementation in JavaScript. The provided code snippet introduces a basic Promise class named `MyPromise` and outlines its key features.

## Understanding Promises

Before delving into the code, let's briefly review the concept of Promises. A Promise is an object representing an asynchronous operation's eventual completion or failure. It is a placeholder for a value that may be available now, or in the future, or never.

The three states of a Promise are:
- **Pending**: The initial state, representing that the promise is neither fulfilled nor rejected.
- **Fulfilled**: The state indicates that the asynchronous operation was completed successfully.
- **Rejected**: The state indicates that the asynchronous operation encountered an error or was unsuccessful.

## Exploring the MyPromise Class

### 1. State Constants

The code begins by defining constants for the three states of a Promise: `PENDING`, `FULFILLED`, and `REJECTED`. These constants provide clarity and improve code readability by avoiding magic strings.

```javascript
const STATE = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
};
```

### 2. MyPromise Class Overview

The `MyPromise` class is then introduced, encapsulating the behavior of a basic Promise. It includes private properties for state, value, and callback functions for fulfillment and rejection. The constructor takes an `executorFunc` function, which is expected to be provided by the user and contains the asynchronous logic.

```javascript
class MyPromise {
  #state = STATE.PENDING;
  #value = null;
  #fulfilledCallbacks = [];
  #rejectedCallbacks = [];

  constructor(executorFunc) {
    // Implementation details...
  }

  // Additional methods...
}
```

### 3. Executor Function

The constructor executes the `executorFunc`, passing in `resolve` and `reject` functions. The `resolve` function transitions the promise to the fulfilled state, while the `reject` function transitions it to the rejected state. If any errors occur during execution are caught and lead to a rejection.

```javascript
constructor(executorFunc) {
  try {
    executorFunc(
      value => this.#resolve(value),
      value => this.#reject(value)
    );
  } catch (error) {
    this.#reject(error);
  }
}
```

### 4. Chaining Promises with `then` Method

The `then` method facilitates the chaining of promises. It takes two optional callback functions: `onFulfilled` and `onRejected`. When the promise is fulfilled or rejected, the corresponding callbacks are invoked, and a new promise is returned.

```javascript
then(onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {
    // Implementation details...
  });
}
```

### 5. Handling Callbacks in `then`

The `fulfilledCallback` and `rejectedCallback` functions are defined to handle the invocation of the user-supplied callback functions. These callbacks are executed using `queueMicrotask` to ensure they are performed asynchronously.

```javascript
const fulfilledCallback = _ => {
  // Implementation details...
};

const rejectedCallback = _ => {
  // Implementation details...
};
```

### 6. State Transition Handling in `then`

Depending on the current state of the promise, the `fulfilledCallback` and `rejectedCallback` are either pushed to their respective arrays or immediately invoked. This ensures proper handling of callbacks in various promise states.

```javascript
switch (this.#state) {
  case STATE.PENDING:
    // Add callbacks to arrays...
    break;
  case STATE.FULFILLED:
    // Invoke fulfilled callback...
    break;
  case STATE.REJECTED:
    // Invoke rejected callback...
    break;
  default:
    throw new Error("Unexpected promise state");
}
```

### 7. Error Handling with `catch` Method

The `catch` method is a shorthand for handling only the rejection case. It internally calls `then` with `null` for `onFulfilled` and the provided `onRejected` callback.

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

### 8. Getters for State and Value

The class provides getters for accessing the current state and value of the promise.

```javascript
get state() {
  return this.#state;
}

get value() {
  return this.#value;
}
```

### 9. Resolve and Reject Logic

The `#resolve` and `#reject` methods are responsible for transitioning the promise to the fulfilled or rejected state, respectively. They also trigger the execution of registered callback functions.

```javascript
#resolve(value) {
  // Implementation details...
}

#reject(value) {
  // Implementation details...
}
```

## Final MyPromise class
```javascript
const STATE = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
};

class MyPromise {
  #state = STATE.PENDING;
  #value = null;
  #fulfilledCallbacks = [];
  #rejectedCallbacks = [];

  constructor(executorFunc) {
    try {
      executorFunc(
        value => this.#resolve(value),
        value => this.#reject(value)
      );
    } catch (error) {
      this.#reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const fulfilledCallback = _ => {
        if( !onFulfilled ) return resolve(this.#value);
        queueMicrotask( _ => {
          try {
            const value = onFulfilled(this.#value);
            resolve(value);
          } catch (error) {
            reject(error);
          }
        });
      };

      const rejectedCallback = _ => {
        if (! onRejected ) return reject(this.#value);
        
        queueMicrotask( _ => {
          try {
            const value = onRejected(this.#value);
            resolve(value);
          } catch (error) {
            reject(error);
          }
        });
      }
      
      switch(this.#state){
        case STATE.PENDING:
          this.#fulfilledCallbacks.push(fulfilledCallback);
          this.#rejectedCallbacks.push(rejectedCallback);
          break;
        case STATE.FULFILLED:
          fulfilledCallback();
          break;
        case STATE.REJECTED:
          rejectedCallback();
          break;
        default:
          throw new Error("Unexpected promise state");
      }
    });
  }

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

  get state() {
    return this.#state;
  }

  get value() {
    return this.#value;
  }

  #resolve(value) {
    this.#value = value;
    this.#state = STATE.FULFILLED;
    this.#fulfilledCallbacks.forEach(cb => cb());
  }

  #reject(value) {
    this.#value = value;
    this.#state = STATE.REJECTED;
    this.#rejectedCallbacks.forEach(cb => cb());
  }
  
}

exports.MyPromise = MyPromise;

```
## Example Use
```javascript
const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(42);
  }, 1000);
});

myPromise
  .then(value => {
    console.log(value); // Output: 42
    return value * 2;
  })
  .then(value => {
    console.log(value); // Output: 84
    return value + 1;
  })
  .then(value => {
    console.log(value); // Output: 85
  });

```

## Conclusion

In this article, we've explored a simplified implementation of a Promise in JavaScript. Understanding the inner workings of Promises is crucial for mastering asynchronous programming. While the provided `MyPromise` class is basic, it captures the essence of how Promises work and can serve as a foundation for building more sophisticated Promise implementations. Promises are an essential tool for managing asynchronous code and are widely used in modern JavaScript development.