Advanced JavaScript Development: Building Promises from Scratch
Tags: faang, javascript, webdev, tutorial
Published: 2023-01-16 22:16:35

## Introduction

`Promise` is a powerful tool in JavaScript that allows for asynchronous processing and handling of data. It can be a tricky concept to implement, especially during a FAANG interview. However, with the right understanding and approach, you can successfully implement your own `Promise`.

In this article, we will go over how to implement a basic version of a promise during a FAANG interview. The [standard for promise](https://promisesaplus.com/) implementation is called A+, but it includes a huge amount of details, making it almost impossible to implement all of them during a one-hour coding interview. Therefore, we will focus on implementing a basic variation that should be enough to show the interviewer your solving skills.

## Implementation

**TL;DR** see the final code at the end

First, let's define the template for our promise. `Promise` is a JS class that contains two main functions `then()` and `catch()`, as well as two additional static functions `resolve()` and `reject()`.

```js
class MyPromise {
  constructor(executor) {
    // your code here
  }

  then(onFulfilled, onRejected) {
    // your code here
  }

  catch(onRejected) {
    // your code here
  }

  static resolve(value) {
    // your code here
  }

  static reject(value) {
    // your code here
  }
}
```

Now, let's take a look at how promises work and write a basic example of usage:

```javascript
const promise = new MyPromise((resolve, reject) => {
  resolve('Test value');
  console.log('resolved');
});

console.log(promise.status); // should be 'resolved'
console.log(promise.value); // should be 'Test value'
```

We'll start by implementing the constructor. `Promise` can have three different states: `pending`, `fullfiled`, `rejected`. During its creation, we should set the status to pending and then call the function passed to the constructor with parameters `onFullfil` and `onReject`.

```js
constructor(executor) {
  this. Status = 'pending';

  try {
    executor. Call(this, this.onFullfil.bind(this), this.onReject.bind(this));
  } catch(e) {
    this.onReject.call(this, e);
  }
}

onFullfil(value) {
  // your code here
}

onReject (value) {
  // your code here
}
```

Now, the executor is being called. However, after `resolve` is called, the status of the promise remains unchanged. Let's fix it:

```js
onFullfil(value) {
  if (this.status !== 'pending') return;
  this.status = 'fullfiled';
  this.value = value;
}

onReject (value) {
  if (this.status !== 'pending') return;
  this.status = 'rejected';
  this. Value = value;
}
```

Now that the promise has the right value and status, let's revise how chaining of `.then()` works:

```js
promise.then((val) => {
  console.log('Then ', val);
  return 'New value';
}).then((val) => {
  console.log('Second then ', val);
});
```

In order to enable chaining, the `then()` method should return a new instance of the promise. To accomplish this, we can utilize the `new MyPromise` constructor.

Additionally, we should run the appropriate callback, either `onFulfilled` or `onRejected`, based on the current state of the initial promise. The returned value from this callback should then be set as the value of the newly created promise instance. This allows for a chain of promises to be created, with each promise's value being determined by the return value of its corresponding callback.


```js
  then(onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {
    const newPromiseResolve = () => {
      try {
        const res = onFulfilled(this.value);
        resolve(res);
      } catch(e) {
        reject(e);
      }
    };

    const newPromiseReject = () => {
      try {
        if (onRejected) {
          const res = onRejected(this.value);
          resolve(res);
        } else {
          reject(this.value);
        }
      } catch(e) {
        reject(e);
      }
    };

    if (this.status === 'fullfiled') {
      newPromiseResolve();
    }

    if (this.status === 'rejected') {
      newPromiseReject();
    }
  });
}
```

While the implementation of the `then()` method appears to be functioning as intended, it's important to consider the scenario where the resolve function is called `async`. In order to properly handle this case, we can write a test case to evaluate the behavior of the promise in this case.

```js
const asyncedPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    console.log('Async resolve');
    resolve('I am async value');
  }, 1000);
});

asyncedPromise.then((val) => {
  console.log('Asynced then', val);
});
```

When the `then()` method is called on a promise that is still in the `pending` state, our current implementation may not handle this scenario properly. To address this, we should save any callbacks passed to the `then()` method in separate arrays for `resolve` and `reject` callbacks. This way, when the promise's state changes from `pending` to either `fulfilled` or `rejected`, we can run the appropriate callbacks that were previously stored. To implement this, we can initialize two arrays, `resolveCallbacks` and `rejectCallbacks`. 

```js
this.resolveCallbacks = [];
this.rejectCallbacks = [];

this.tasks = [];
```

Then add one more condition for a `pending` status in `.then()` function. We need to save passed callbacks to our inner state.

```js
if (this.status === 'pending') {
  this.resolveCallbacks.push(newPromiseResolve);
  this.rejectCallbacks.push(newPromiseReject);
}
```

Now we need to write function `runTasks()` to run all saved callbacks and clean arrays after.

```js
doTask () {
  const task = this.tasks.shift();
  if (task) {
    task.call(this, this.value);
  }
}

runTasks () {
  while(this.tasks.length) {
    this.doTask();
  }
}
```

Let's call `runTasks` function inside `onFulfill` and `onReject`:

```js
onFullfil(value) {
  if (this.status !== 'pending') return;
  this.status = 'fullfiled';
  this.value = value;

  this.tasks.push(...this.resolveCallbacks);
  this.resolveCallbacks = [];
  this.runTasks();
}

onReject (value) {
  if (this.status !== 'pending') return;
  this.status = 'rejected';
  this.value = value;

  this.tasks.push(...this.rejectCallbacks);
  this.rejectCallbacks = [];
  this.runTasks();
}
```

Congratulations, the implementation of the `then()` method now works `asynchronously`. However, we must not forget an important consideration when chaining multiple `then()` calls together. If a promise instance is returned within the `onFulfilled` or `onRejected` callback passed to the `then()` method, it should be handled correctly by the next `then()` in the chain. According to the Promise specification, the next `then()` in the chain should wait for the returned promise to resolve before proceeding, and the final resolved value should be passed as the input to the next `then()` rather than the promise instance.

Example: 

```js
promise.then(
  (val) => new MyPromise((res) => res('Value from promise'))
).then(
  (val) => console.log(val) // should print 'Value from promise' string, not promise instance
)
```

Let's check if `res` is a promise instance or just a value.

```js
if (res instanceof MyPromise) {
  res.then(resolve, reject); // if it's a promise instance wait for value and resolve/reject
} else {
  resolve(res); // otherwise just resolve value
}
```

Now comes the easiest part. Let's implement the static methods. We simply need to create a promise and instantly `resolve` or `reject` its value.

```js
static resolve(value) {
  return new MyPromise((resolve) => resolve(value));
}

static reject(value) {
  return new MyPromise((_resolve, reject) => reject(value));
}
```

And the method `catch()`:

```js
catch(onRejected) {
  return this.then((value) => value, onRejected);
}
```

## The bonus part

Let's verify that all callbacks are called in the correct order by providing an example:

```js
// Test: constructor should be sync, then handlers should be async
// order should be: 0, 1, 3, 2

console.log(0);
new MyPromise((resolve, reject) => {
  console.log(1);
  resolve(1);
}).then(() => {
  console.log(2)
});
console.log(3);
```

If we run this test we will see `0, 1, 2, 3` order. But it's expected to be `0, 1, 3, 2`.

It happens because callbacks inside `.then()` must be called async. Let's wrap it with `setTimeout` to make it async.


```js
setTimeout(() => {
  newPromiseResolve();
}, 0);

setTimeout(() => {
  newPromiseReject();
}, 0);
```

## Summary

In summary, implementing a basic variation of a `Promise` class involves creating a class with a constructor, methods to `resolve` and `reject` the promise, and methods to handle the callbacks passed to the `then()` and `catch()` methods. Additionally, you need to make sure that callbacks passed to the `then()` method are called asynchronously and in the correct order.

## Final version of the code:

```js
class MyPromise {
  constructor(executor) {
    this.status = 'pending';

    this.resolveCallbacks = [];
    this.rejectCallbacks = [];

    this.tasks = [];

    try {
      executor.call(this, this.onFullfil.bind(this), this.onReject.bind(this));
    } catch(e) {
      this.onReject.call(this, e);
    }
  }

  doTask () {
    const task = this.tasks.shift();
    if (task) {
      task.call(this, this.value);
    }
  }

  runTasks () {
    while(this.tasks.length) {
      this.doTask();
    }
  }

  onFullfil(value) {
    if (this.status !== 'pending') return;
    this.status = 'fullfiled';
    this.value = value;

    this.tasks.push(...this.resolveCallbacks);
    this.resolveCallbacks = [];
    this.runTasks();
  }

  onReject (value) {
    if (this.status !== 'pending') return;
    this.status = 'rejected';
    this.value = value;

    this.tasks.push(...this.rejectCallbacks);
    this.rejectCallbacks = [];
    this.runTasks();
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const newPromiseResolve = () => {
        try {
          const res = onFulfilled(this.value);

          if (res instanceof MyPromise) {
            res.then(resolve, reject);
          } else {
            resolve(res);
          }
        } catch(e) {
          reject(e);
        }
      };

      const newPromiseReject = () => {
        try {
          if (onRejected) {
            const res = onRejected(this.value);

            if (res instanceof MyPromise) {
              res.then(resolve, reject);
            } else {
              resolve(res);
            }
          } else {
            reject(this.value);
          }
        } catch(e) {
          reject(e);
        }
      };

      if (this.status === 'pending') {
        this.resolveCallbacks.push(newPromiseResolve);
        this.rejectCallbacks.push(newPromiseReject);
      }

      if (this.status === 'fullfiled') {
        setTimeout(() => {
          newPromiseResolve();
        }, 0);
      }

      if (this.status === 'rejected') {
        setTimeout(() => {
          newPromiseReject();
        }, 0);
      }
    });
  }

  catch(onRejected) {
    return this.then((value) => value, onRejected);
  }

  static resolve(value) {
    return new MyPromise((resolve) => resolve(value));
  }

  static reject(value) {
    return new MyPromise((_resolve, reject) => reject(value));
  }
}
```