Understanding JavaScript Promises

Introduction

What are Promises?

  • Managing Asynchronous Operations: In JavaScript, many operations (like fetching data from a server, reading a file, or waiting for a timer) take time. Promises provide a structured way to handle the results of these asynchronous operations without getting tangled up in messy callbacks.
  • A Proxy for Future Values: A Promise is an object that represents the eventual result of an asynchronous operation. It’s like a placeholder. Initially, the promise is in a “pending” state, but eventually, it will either:
    • Fulfilled: The operation was successful, and a value is available.
    • Rejected: The operation failed, and you get an error explaining why.

Key Methods

  • .then() Used to handle the successful resolution of a Promise. It takes a callback function that receives the resolved value.
  • .catch() Used to handle errors. It takes a callback function that receives the error object.
  • .finally() Executes a callback function regardless of whether a promise is fulfilled or rejected. Often used for cleanup tasks.
 import console from 'console';
 import { setTimeout } from 'timers';

function delay(ms: number, shouldResolve: boolean): Promise<string> {
  
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldResolve) {
        resolve('Promise resolved');
      } else {
        reject('Promise rejected');
      }
    }
    , ms);
  });
}

delay(1000, true).then(function(message) {
  console.log(message); // This will log "Promise resolved"
}).catch(function(error) {
  console.error(error); // This won't be called in this case
});

When you create a new Promise in TypeScript, you pass an executor function to the Promise constructor. This executor function takes two arguments: a resolve function and a reject function.

Here’s what each argument is for:

  • resolve: This is a function that you call when the asynchronous operation completes successfully. You call this function with the result of the operation.
  • reject: This is a function that you call when the asynchronous operation fails. You call this function with the reason for the failure, which is typically an Error object.

(resolve, reject) => { ... }

  • This is the executor function. It has two parameters:
    • resolve: A function you call when the asynchronous operation has successfully completed. You must pass it the value that the Promise should resolve to.
    • reject: A function you call if the asynchronous operation fails. You pass it an Error object that explains the reason for the failure.

Code for JavaScript Promises

class MyPromise {
    constructor(executor) {
        this.state = 'pending';
        this.value = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = (value) => {
            if (this.state === 'pending') {
                if (value instanceof MyPromise) {
                    value.then(resolve, reject);
                } else {
                    this.state = 'fulfilled';
                    this.value = value;
                    this.onFulfilledCallbacks.forEach(callback => callback(value));
                }
            }
        };

        const reject = (reason) => {
            if (this.state === 'pending') {
                this.state = 'rejected';
                this.value = reason;
                this.onRejectedCallbacks.forEach(callback => callback(reason));
            }
        };

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

    then(onFulfilled, onRejected) {
        return new MyPromise((resolve, reject) => {
            const handleFulfilled = (value) => {
                try {
                    const result = onFulfilled(value);
                    if (result instanceof MyPromise) {
                        result.then(resolve, reject);
                    } else {
                        resolve(result);
                    }
                } catch (error) {
                    reject(error);
                }
            };

            const handleRejected = (reason) => {
                try {
                    const result = onRejected(reason);
                    if (result instanceof MyPromise) {
                        result.then(resolve, reject);
                    } else {
                        reject(result);
                    }
                } catch (error) {
                    reject(error);
                }
            };

            if (this.state === 'fulfilled') {
                handleFulfilled(this.value);
            } else if (this.state === 'rejected') {
                handleRejected(this.value);
            } else {
                this.onFulfilledCallbacks.push(handleFulfilled);
                this.onRejectedCallbacks.push(handleRejected);
            }
        });
    }
}

Design Choices

  • The MyPromise class has a constructor that takes an executor function. This function is immediately executed and is passed two functions: resolve and reject. These functions allow the executor to indicate the success or failure of the promise.
  • The state property is used to track the status of the promise. It can be ‘pending’, ‘fulfilled’, or ‘rejected’.
  • The value property is used to store the result of the promise.
  • The onFulfilledCallbacks and onRejectedCallbacks arrays are used to store callback functions that will be executed when the promise is fulfilled or rejected.
  1. New Promise from then function:
    • The then method returns a new promise. This is a key feature of promises that enables chaining. The returned promise resolves or rejects based on the outcome of the onFulfilled or onRejected callbacks. If these callbacks return a value, the returned promise is resolved with that value. If they throw an error, the returned promise is rejected with that error.
  2. Need for a Callback Array:
    • The onFulfilledCallbacks and onRejectedCallbacks arrays are needed because the then method can be called multiple times on the same promise. Each time then is called, a new callback is added to the respective array. When the promise is settled (either fulfilled or rejected), all the registered callbacks are called. This is known as “observation” of a promise.
    • These arrays are also useful when the then method is called after the promise has already settled. In this case, the callback is immediately called with the promise’s value or reason

myPromise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('success');
    }, 1000);
});

myPromise.then((value) => {
    console.log(value);
});

Leave a comment