You Don't Know Promises

You Don't Know Promises

Building JavaScript Promises from Scratch

The concept of Promises was by far the most confusing topic for me. Async-Await-ing Axios calls to complete features left me in a delusion that I know promises. Until recently, in an interview, I was asked to build the Promises part of the JavaScript from scratch. My first response was, "Hold on, What?". Obviously, that's not what I told the interviewer, instead I rolled up my sleeves and jumped into the grill which the interviewer had kept heating for me. Fast forward to what were the most embarrassing 30 minutes of my life, I was able to pass 3/6 tests and we moved on to the next question. I usually suck at moving on, so here I am, trying to attempt that question again and this time with full effort hoping to fill my knowledge gaps on the way as I do it, I hope this article fills all your gaps as well.

Anatomy of Promise:
Like everything else in the JS world, Promise is also an object and has a few properties.

This is basically how you create a Promise.

const brandNewPromise = new Promise((resolve, reject)=>{ 
      // some async operation happens here 
      if(operationSucceeds){
        resolve(dataReturnedFromTheOperation);
     }
    else{ 
        reject(reasonWhyTheOperationWasRejected);
    }
})

But creating a promise is only job half done, the way you use a Promise is

brandNewPromise.then((data)=>{ 
    //using the data returned here 
}) 

brandNewPromise.catch((reason)=>{ 
    // knowing why the async operation involved failed, 
    // and what to do with it 
})

Another way is, instead of explicitly using promises, you can fetch the data asynchronously in this way. In this case, we are using Promises under the hood, where the function (parent not the child) execution pauses until the child function ( someOperation ) returns a fulfilled promise. Yes, you read it right, the function here must return a promise for this to work. The only thing the below code does is, wait until the returned promise is fulfilled in a much cleaner way compared to before.

One beautiful thing to observe here is that the "await" keyword implicitly attaches a then handler to our Promise, it's as if like, it's doing the work of calling then and returning the resolved data ( The data that is passed on to the resolve function ) with it. A well-needed syntactic sugar IMHO. Or maybe I suspect, the ECMA peeps thought the original way of Promises was too complicated and unnecessary and hence came up with this gem.

const someAsyncFunction = async () => { 
try { 
const data = await someOperation();
} catch(error){  
  // here the rejected function code can be executed  
  errorHandler(error);
}
}

With the structure out of our way, a quick example of how to actually use Promises using the above methods.
The first one is the traditional way of using Promises, via the constructor provided.

const traditionalPromise = new Promise((resolve, reject) => {
  try {
    fetch("https://jsonplaceholder.typicode.com/todos/1")
      .then((response: Response) => response.json())
      .then((data) => resolve(data))
      .catch((err) => {
        throw err;
      });
  } catch (error) {
    reject(error);
  }
});

traditionalPromise.then((data) => {
  console.log("Data Returned is ", data);
});

traditionalPromise.catch((err) => {
  console.error("Promise failed due to", err);
});

The second one is using the Async-Await way of handling promises

const betterPromiseHandling = async () => {
  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos/1"
    );
    const data = await response.json();
    console.log("Data is", data);
  } catch (error) {
    console.error("Error Throw is", error);
  }
};

betterPromiseHandling();

Needless to say, the one that's preferred is the second one because it gets the job done and looks quite clean and concise.

I know you might be thinking that I baited you into building your promises but instead, telling you what already know, before you close this tab let's get right into it.

Promises Mental Modal

So, broadly speaking, Promises are JS's way of attaching a function when something async happens and another function when it does not.

To build Promises from Scratch, we first need to create a Promise class, thanks to ES6, we can do that using JS. Although, I'd like to use TS ( Sorry, DHH)

What we know:

  1. Promise is a JS object.

  2. Promise takes a callback as an argument, which gets called as soon as it's created.

  3. The callback is nothing but a function, which has two parameters, "resolve" and "reject".

  4. resolve and reject are functions by themselves.

  5. Resolve is called for a successful scenario, which gets called with the response of the promise once it's fulfilled.

  6. Reject is called for the rejected scenario, which gets called with the reason as to why the Promise has failed.

class customPromise { 
    constructor(callback: (resolve, reject)=> void ){
       try { 
            callback(resolve, reject);
        }
        catch(error){ 
            reject(error);
        }
    }

The above code might be confusing at first ( well, it was at least for me ). It means that the callback that you pass on to the promise is simply called along with the resolve and reject handlers. Remember, resolve and reject are again callbacks themselves that are called for success or failure scenarios.

But, the point is to call the handlers with their respective parameters, i.e call the reject callback with the error that occurs and resolve callback with the data that gets resolved.

resolve(data)

newPromise.then((data)=>{})  

// When you call the resolve callback with the data, the same data gets
// passed on to the "then" callback below.  

reject(error) 

newPromise.catch((error)=>{}) 

// Same is the case with the error callback.

If you counter this, our original implementation extends to

class customPromise { 
    constructor(callback: (resolve, reject)=> void ){
       try { 
            callback(
        (data)=>resolve(data), 
        (error)=> reject(error) 
        );
        }
        catch(error){ 
            reject(error);
        }
    }
}

This resolves the callback part ( bad pun, I know ). But we'll also have to consider the case when "then" and "catch" are attached as handlers. So, for that, we have can dedicated handler variables and simply assign the handler functions to those.

Which makes our implementation something like

    export default class CustomPromise {
        private resolveHandler : Function = ()=> {};
        private rejectHandler : Function = ()=> {};
        constructor(callback : (resolve: Function, reject: Function)=>void){
            try {
                callback( 
                (data:any)=>this.resolveHandler(data),
                (error:any)=>this.rejectHandler(error)
                );

            } catch (error) {
                this.rejectHandler(error);
            }        
        }


    then(callback: Function): CustomPromise {
        // returning new promise object from the then enables chaining 
        return new CustomPromise((resolve, reject) => {
            this.resolveHandler = (data: any) => {
                try {
                    resolve(callback(data));
                } catch (error) {
                    reject(error);
                }
            };
        });
    }

        catch(callback: Function): CustomPromise {
      // same thing with the catch methods as well
            return new CustomPromise((resolve, reject) => {
                this.rejectHandler = (error: any) => {
                    reject(callback(error));
                };
            });
        }
    }

Now testing part, I've used the above the class in the place of original promise for a simple API call, and it WORKED!

import CustomPromise from "./index.ts";

const newPromise = new CustomPromise(
    (resolve, reject)=>{
    try {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
        .then(response=> response.json())
        .then(data=> resolve(data))
    } catch (error) {
        reject(error);
    }
}
);

newPromise.then((result:any)=>{
    console.log("Promise result is", result);
})

newPromise.catch((error:any)=>{
    console.log("Promise Error is", error);
})

I'm sure the actual JS lib implementation done by the ECMA peeps is much more complicated, but I'm also sure that this minor building up from scratch gave you an idea how Promises built internally.

If you liked my article, then always stick to the promises you've made ( Another bad pun :P ).

Did you find this article valuable?

Support Kunal Dubey by becoming a sponsor. Any amount is appreciated!