Blog/NotesConcept

Promise Polyfill in JavaScript - Step by Step Explanation

An Interview-focused explanation of Promise Polyfill in JavaScript which helps to understand both Functional and ES6 custom promise implementation.

Expert

Anuj Sharma

Last Updated Dec 27, 2024


Promise polyfill or custom promise implementation is an essential concept to understand for frontend interviews. Implementation of promise polyfill is complex and in this guide, we will try to simplify this complex implementation by providing a step-by-step explanation. 

Jump directly to the topic

How does Promise in JavaScript work?

Before diving deep into the promise polyfill in javascript it is important to understand the execution of the promise and different use cases related to then, catch, resolve and reject. This will help write custom promises and test the polyfill against the required use cases.

Promise example with resolve()

Example

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Call is resolved');
  }, 1000);
});

promise.then(value => {
  console.log('Value after 1 sec - ', value);
});

Output:
Value after 1 sec - Call is resolved

Promise example with reject() 

Example 1: Handle rejected promise using Promise.then()

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('Error happened !!'));
  }, 1000);
});

promise2.then(
  value => {
    console.log('Value after 1 sec - ', value);
  },
  error => {
    console.log('Error - ', error.message);
  }
);

Output
Error - Error happened !!

Example 2: Handle rejected promise using Promise.catch()

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('Error happened !!'));
  }, 1000);
});

promise2
.then(value => {
  console.log('Value after 1 sec - ', value);
})
.catch(
  error => {
    console.log('Error from catch - ', error.message);
  }
)

Output:
Error from catch - Error - Error happened !!

Example 3: Ignore catch if the error has already been handled by then previously in the code.

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('Error happened !!'));
  }, 1000);
});

promise2.then(
  value => {
    console.log('Value after 1 sec - ', value);
  },
  error => {
    console.log('Error - ', error.message);
  }
).catch(
  error => {
    console.log('Error from catch - ', error.message);
  }
)

Output
Error - Error happened !!

Functional Implementation of Promise Polyfill in JavaScript

The functional implementation contains the custom promise function and constructor

Step by Step Explanation

  • Step 1: Create the required identifiers to manage the promise state.
    • PENDING: This is the initial state of the promise when the promise has been created but the associated asynchronous function has not yet succeeded or failed.
    • FULFILLED: This state represents the successful completion of the promise. then() is executed at this state.
    • REJECTED: Represents the failed promise state. The catch() execution happened in this state if it presents.
    • successHandlers: Array to catch all the success callbacks passed as part of then().
    • errorHandlers: Array to capture the catch callback.
  • Step 2: The whole execution of CustomPromise started with executing the passed executor callback function. Resolve and reject functions passed by the promise implementation.
  • resolve() implementation: resolve() invoked as part of the executor. It invokes only when the current state is "PENDING". As part of its invocation, it changes the "PENDING" state to "FULFILLED" and execute all the success handlers which was part of the then callback function. 
  • reject() implementation: reject() invoked as part of the executor. It invokes only when the current state is "PENDING". As part of its invocation, it changes the "PENDING" state to "REJECTED" and execute all the catch() handlers or then() error handlers which was part of either catch() callback function or then() callback function. 
  • then() implementation: This is the most crucial implementation as part of Promise. It return another promise to enable promise chaining. As part of the first step of invocation it return a new instance of the promise.
    At the start when the promise state is pending, it keeps all the success callback functions as part of successHandler Array. This is the same array which got invoked when resolved it executed. When the promise state is FULFILLED, it invokes the handleSuccess Asynchronously. 
  • catch() implementation: Catch implementation is straight forward, catch is just a syntactic sugar on top of the then() where we invoke the then() with successCallback as null and only pass the errorCallback like then(null, errorCallback)

Functional Implementation Code of Promise Polyfill

function CustomPromise(executor) {
  // Step 1
  const PENDING = 'pending';
  const FULFILLED = 'fulfilled';
  const REJECTED = 'rejected';

  let state = PENDING;
  let value = null;
  // Store the success callbacks
  let successHandlers = [];
  // Store the error catch callbacks
  let errorHandlers = [];

  function resolve(val) {
    if (state === PENDING) {
      state = FULFILLED;
      value = val;
      // Execute all success handlers
      successHandlers.forEach(fn => fn(value));
    }
  }

  function reject(reason) {
    if (state === PENDING) {
      state = REJECTED;
      value = reason;
      // Execute all error handlers
      errorHandlers.forEach(cb => cb(value));
    }
  }

  // "then" method returns a promise
  this.then = function (callback) {
    return new CustomPromise(function (resolve, reject) {
      
      // Handle the success callback asynchronously.
      function handleSuccess() {
        try {
          const result = callback ? callback(value) : value;
          //check if the result of then is also a promise
          if (result && typeof result.then === 'function') {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (err) {
          reject(err);
        }
      }

      if (state === FULFILLED) {
        // Call handleSuccess asynchronously
        setTimeout(handleSuccess, 0);
      } else if (state === PENDING) {
        successHandlers.push(handleSuccess);
      }
    });
  };

  // The catch method to add error handlers (syntactic sugar for .then(null, errorCallback))
  this.catch = function (errorCallback) {
    return this.then(null, errorCallback);
  };

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

Example to execute function based CustomPromise Polyfill

const executor = (resolve, reject) => {
  console.log('generating number...');
  setTimeout(() => {
    let randomNumber = Math.floor(Math.random() * 10);

    if (randomNumber < 10) {
      resolve(`Random number generated successfully ${randomNumber}.`);
    } else {
      reject('Failed to generate number less than 10 !!');
    }
  }, 1000);
};

const generateNumber = new CustomPromise(executor);

generateNumber.then(result => {
  // Random number generated successfully <random Number>
  console.log(result);
});

generateNumber.catch(error => {
  // Failed to generate a number less than 10 !!
  console.log(error);
});

ES6 Implementation of Promise Polyfill in JavaScript

This ES6 implementation contains the custom promise implementation using ES6 classes.

Step by Step Explanation

  • Step 1: Declare required Identifiers - Create the required static identifiers as part of the class to manage the promise state.
    • PENDING: This is the initial state of the promise when the promise has been created but the associated asynchronous function has not yet succeeded or failed.
    • FULFILLED: This state represents the successful completion of the promise. then() is executed at this state.
    • REJECTED: Represents the failed promise state. The catch() execution happened in this state if it presents.
    • successHandlers: Array to catch all the success callbacks passed as part of then().
    • errorHandlers: Array to capture the catch callback.
  • Step 2: Invoke executor function - The whole execution of CustomPromise started with the class constructor, which takes the executor function as input and pass both resolve and reject functions while invoking the same. Invocation of resolve and reject will determine whether promise is FULFILLED or REJECTED.  
  • resolve() implementation: resolve() invoked as part of the executor in constructor. It invokes only when the current state is "PENDING". As part of its invocation, it changes the "PENDING" state to "FULFILLED" and execute all the success handlers which was part of the then callback function. 
  • reject() implementation: reject() invoked as part of the executor. It invokes only when the current state is "PENDING". As part of its invocation, it changes the "PENDING" state to "REJECTED" and execute all the catch() handlers or then() error handlers which was part of either catch() callback function or then() callback function. 
  • then() implementation: This is the most crucial implementation as part of Promise. It return another promise to enable promise chaining. As part of the first step of invocation it return a new instance of the promise.
    At the start when the promise state is pending, it keeps all the success callback functions as part of successHandler Array. This is the same array which got invoked when resolved it executed. When the promise state is FULFILLED, it invoks the handleSuccess Asynchronously. 
  • catch() implementation: Catch implementation is straight forward, catch is just a syntactic sugar on top of the then() where we invoke the then() with successCallback as null and only pass the errorCallback like then(null, errorCallback)

ES6 Implementation code of Promise Polyfill

class CustomPromise {
  // Step 1
  static STATE = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected',
  };

  constructor(executor) {
    this.state = CustomPromise.STATE.PENDING;
    this.value = null;
    this.successHandlers = [];
    this.errorHandlers = [];

    const resolve = (val) => {
      if (this.state === CustomPromise.STATE.PENDING) {
        this.state = CustomPromise.STATE.FULFILLED;
        this.value = val;
        this.successHandlers.forEach(callback => callback(this.value));
      }
    };

    const reject = (reason) => {
      if (this.state === CustomPromise.STATE.PENDING) {
        this.state = CustomPromise.STATE.REJECTED;
        this.value = reason;
        this.errorHandlers.forEach(callback => callback(this.value));
      }
    };
    
    // Step 2
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(callback) {
    return new CustomPromise((resolve, reject) => {
        const handleSuccess = ()=>{
            try{
                const result = callback ? callback(this.value) : this.value;
                if(result && typeof result.then === 'function'){
                    result.then(resolve, reject)
                }else{
                    resolve(result)
                }
            }catch(err){
                reject(err);
            }
        }
      if (this.state === CustomPromise.STATE.FULFILLED) {
          setTimeout(handleSuccess, 0);
      } else if (this.state === CustomPromise.STATE.PENDING) {
        this.successHandlers.push(handleSuccess);
      }
    });
  }

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

Example usage of ES6 based CustomPromise polyfill 

const executor = (resolve, reject) => {
  console.log('generating number...');
  setTimeout(() => {
    let randomNumber = Math.floor(Math.random() * 10);

    if (randomNumber < 7) {
      resolve(`Random number generated successfully ${randomNumber}.`);
    } else {
      reject('Failed to generate number less than 10 !!');
    }
  }, 1000);
};

const generateNumber = new CustomPromise(executor);

generateNumber.then(result => {
  // Random number generated successfully <random Number>
  console.log(result);
});

generateNumber.catch(error => {
  // Failed to generate a number less than 10 !!
  console.log(error);
});

📢Final Note:

Implementing Promise polyfill in JavaScript is the most common interview question in experienced frontend interviews. Its important to map the actual working of the polyfill with the implementation of the Promise Polyfill to understand it fully. This way you need not to keep the whole implementation in mind rather you can derive the whole Promise Polyfill implementation on the fly using the usage pattern of the Promise in JavaScript.

Hope you will be able to understand the Promise Polyfill in JavaScript implementation in depth to Ace the next frontend interview.


Share this post now:

Flaunt You Expertise/Knowledge & Help your Peers

Sharing your knowledge will strengthen your expertise on topic. Consider writing a quick Blog/Notes to help frontend folks to ace Frontend Interviews.

Other Related Blogs

4 Ways to Reverse a String in JavaScript (JavaScript Interview)

Anuj Sharma

Last Updated Jan 4, 2025

Explore the most common ways to reverse a string in javascript including the most optimal way for frontend interviews with O(1) time complexity.

What is javascript:void(0) and How it Works?

Anuj Sharma

Last Updated Jan 5, 2025

A comprehensive explanation about using javascript:void(0) in javascript. When to use javascript:void(0) and how it works with examples of using it with anchor tag.

Explained Web Authorization Techniques - Session & JWT

Anuj Sharma

Last Updated Jan 2, 2025

Understand important web authorization techniques to enhance role-based authentication for any web application with popular techniques like Session & JSON Web Token (JWT)

Ultimate guide to REST API calls using Fetch: Machine Coding Essential

Vivek Chavan

Last Updated Dec 23, 2024

You will get a clear understanding about working with any rest api and common concepts asked during interviews

Understand JavaScript Date Object with Examples (for JavaScript Interviews)

Anuj Sharma

Last Updated Jan 9, 2025

Go through different ways to display dates using javascript date object. It covers examples of date object usage to understand the main concepts of javascript date object.

What is CORS ? Cross-Origin Resource Sharing Explained [For Interviews]

Anuj Sharma

Last Updated Dec 10, 2024

A brief explanation of Cross-Origin Resource Sharing (CORS) concept to enable client application accessing resources from cross domain and HTTP headers involved to enable resource access.

FrontendGeek
FrontendGeek

© 2024 FrontendGeek. All rights reserved