BlogsAboutNow

Promise Chaining


Let's consider three functions asyncCallbackFn1, asyncCallbackFn2, and asyncCallbackFn3.


    // Function definition

    type Callback = (error: Error | null, data: any) => void;
    type AsyncCallbackFn = (callback: Callback, data: any) => void;

    const asyncCallbackFn1: AsyncCallbackFn = (callback, num) => {
        setTimeout(() => callback(null, num * 2), 100)
    };

    const asyncCallbackFn2: AsyncCallbackFn = (callback, num) => {
        setTimeout(() => callback(null, num * 3), 100)
    };

    const asyncCallbackFn3: AsyncCallbackFn = (callback, num) => {
        setTimeout(() => callback(null, num * 7), 100)
    };

We have to create a function called sequence() which chains up these async callback functions in sequential manner.

Something like:


    const sequence = (funcs: AsyncCallbackFn[]): AsyncCallbackFn => {
        // Your code here
    }

    const result = sequence([asyncCallbackFn1, asyncCallbackFn2, asyncCallbackFn3]);

    result((error, data) => {
        console.log(data) // here data is the answer : 42
    }, 1)

sequence() accepts an array of AsyncCallbackFn and chains them up by passing resulting data to the next AsyncCallbackFn. Chains up means call one function and pass the result to the next function and so on. Just like a chain-reaction.


Let's dig into it 🔍

Let's break down the problem statement. We know that sequence() accepts an array of functions and returns a function of type AsyncCallbackFn.


     const sequence = (funcs: AsyncCallbackFn[]): AsyncCallbackFn => {
        // Let's first return a function of type AsyncCallbackFn
        return function(callback, data){
            // Your code here
        }
    }

So the next step would be to write what's happening inside the returned function. That's the heart of the entire question.

Since the parameter of sequence() is an array of functions, to get the result, we would iterate through them.


     const sequence = (funcs: AsyncCallbackFn[]): AsyncCallbackFn => {
         return function(callback, data){
            let index = 0;
            // some loop
            funcs[index](callback, data);
            index++;
         }
     }

We know that the result of the first function funcs[0] (representing asyncCallbackFn1) would be passed to funcs[1] (representing asyncCallbackFn2) and so on. And if there is an error at any step, we can terminate the loop and return the error.


   let index = 0;
   // some loop

   funcs[index]((error, data) => { // expanding the callback function
        if (error) {
            // if error, return null as data
            callback(error, null);
        }
        else {
            // call the next funcs and pass data
            // funcs[nextIndex](data); // data is the result of funcs[0]
            // This is recursion here
        }
   },
   data); // initial data
   index++;

As we can see it's a case of recursion, so let's simplify.

Wrapping the entire functionality inside nextCallback() function that will handle the task of taking the data and passing it to the next function.


   let index = 0;
   const nextCallback = (data: number) => {
       // do something
       // call nextCallback(with_some_data) again
       index++;
   };

   nextCallback(initialData)

So let's write up nextCallback logic and put the solution together


    const sequence = (funcs: AsyncFunc[]): AsyncFunc => {
        return function(callback, initialData){
            let index = 0;

            // a recursive function
            const nextCallback = (data: number) => {

                // break condition:
                // once reached the end of the loop, return callback with data
                if (index === funcs.length) {
                    callback(null, data);
                    return;
                }

                // loop over funcs and on success call nextCallback with the data
                funcs[index]((error, data) => {
                    if (error) {
                        // break condition
                        callback(error, null);
                    } else {
                        nextCallback(data);
                    }
                }, data);

                // increment counter
                index++;
            };

            nextCallback(initialData)
        }
    }

Now, let's solve this using Promises.

Since the functions don't return Promises, let's modify them to return a Promise first.

This promisify function will return a function that, in turn, returns a Promise.


     // Create a promisify function that takes an AsyncCallbackFn
     const promisify = (fn: AsyncCallbackFn): Function => {

            // Return a function that accepts defaultData and returns a Promise
            return function (defaultData: number): Promise {
                return new Promise((resolve, reject) => {

                    // Call the original function (fn) with a callback
                    fn((error, data) => {
                        if (error) {
                            reject(error); // Reject the Promise if an error occurs
                        } else {
                            resolve(data); // Resolve the Promise with the data
                        }
                    }, defaultData);
                })
            }
         }

Now, let's convert all the functions to promisified functions

 const promisifiedFuncs = funcs.map((fn) => promisify(fn)); 

Now, we need to iterate over promisifiedFuncs and pass the resolved data to the next function.



    for (const promiseFunc of promisifiedFuncs) {
        // do something with the data of promiseFunc here
    }

Side note:

When dealing with Promises, the for...of loop is particularly useful because it allows you to work with asynchronous operations in a more synchronous-like manner. This is especially handy when you need to execute Promises sequentially or handle dependencies between asynchronous tasks.

Check the bonus section for more details.


However, during iteration we require some initial data defaultData (check above) to start with, which will be passed to asyncCallbackFn1. In the first iteration, this initial data will be provided to promiseFunc Hmm… 🤔


    for (const promiseFunc of promisifiedFuncs) {
        // pass the initial data to promiseFunc
        const result = promiseFunc(data);

        // pass the result to next promiseFunc
    }

Where will we get the initial data from and what format should it be in? 🤔

Understand few things first:

  • We have a promiseFunc that returns a Promise.
  • We want to pass the resolved data from promiseFunc to the next promiseFunc, something like
    const result = promiseFunc(data).then(data => /* pass the data to the next Promise */)
  • The result is a Promise.

Now, let's think about how you would accomplish this in a regular loop without using promises, where you need to pass data to the next function. In such a scenario, you might typically do something like this:


    // some_Func() returns a numeric value;
    let result = initialNumericValue; // 0

    for(let i=0; i<5; i++){
        result = result + some_Func(i);
    }

Now keep this concept in mind and let's use the same in case of Promises.

Assign an initial value to result, which will be a resolved Promise.

So let's create an initial Promise that resolves with the data initialData, and then chain that Promise with promiseFunc. Although it may not be a straightforward approach, so isn't the world!


    // Create an initial Promise with the resolved data (initialData)
    // renaming result to promise to keep the context intact with the variable
    let promise = Promise.resolve(initialData);

    for (const promiseFunc of promisifiedFuncs) {
        // Create a chain of promises to be executed later
        promise = promise.then((data) => promiseFunc(data));
    }

Now, let's consume the data or handle the error from the Promise


    // Execute the chain of promises
    promise.then((data) => {
        callback(null, data);
    }).catch((error) => {
        callback(error, null);
    })

So let's put all the steps together


    const sequence = (funcs: AsyncFunc[]): AsyncFunc => {
        return function (callback, initialData) {
            // Promisify each function in the array
            const promisifiedFuncs = funcs.map((fn) => promisify(fn));

            // Initialize the promise with the initialData
            let promise = Promise.resolve(initialData);

            // Chain all promisifiedFuncs together in sequence
            for (const promiseFunc of promisifiedFuncs) {
                promise = promise.then((data) => promiseFunc(data));
            }

            // Resolve the final promise and invoke the callback with the result
            promise.then((data) => {
                callback(null, data);
            }).catch((error) => {
                callback(error, null);
            })
        }
    }

Annnd we are done!! 🎉


Bonus 🚀

The same can be solved with async/await

For solving such an iterative problem with async/await, we can use for of loop with await

Let's start with a simple example:


    // Async functions representing items
    const func1 = async () => {
        return new Promise((resolve) => {
            setTimeout(() => {
                resolve('Yayy!');
            }, 1000);
        });
    };

    const func2 = async () => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('Oops!');
            }, 2000);
        });
   };

   const funcs = [func1, func2];
   solve(funcs);

If we need to iterate over these items, we can use a for...of loop


    async function solve(funcs) {
        for (const fn of funcs) {
            try {
                const result = await fn();
                // Do something with result
            } catch (error) {
                // Do something with error
            }
        }
    }

In simpler terms:

The for...of loop with await helps you handle a list of tasks one at a time, waiting for each task to finish before moving on to the next.


Now we can apply the same knowledge to solve our original question.


     const sequence = (funcs: AsyncCallbackFn[]): AsyncCallbackFn => {
        return async function(callback, initialData) {

            // Promisify each function in the array
            const promisifiedFns = funcs.map((fn) => promisify(fn));

            try {
                // Iterate over the promisified functions sequentially
                for (const promiseFn of promisifiedFns) {
                    // Await the result of each function and update the initialData
                    initialData = await promiseFn(initialData);
                }

                // Invoke the callback with the final result
                callback(null, initialData);
            } catch (error: any) {
                 // If an error occurs, invoke the callback with the error
                callback(error, null);
            }
        }
     }

Let's try with reduce and async/await

Let's use the same example as above to understand how reduce works with async functions.


    async function solve(funcs) {
        try{
            const promise = Promise.resolve(undefined);

            // Execute the async functions sequentially using reduce
            const result = await funcs.reduce((previousPromise, currentFunc) => {
                // Wait for the previous promise to resolve and store the result
                const previousResult = await previousPromise;

                // Execute the current function and get its result
                return currentFunc().then((currentResult) => [previousResult, currentResult]);
            }, promise);

            console.log('Result:', result);

        } catch (error) {
            console.log('Error:', error);
        }
    }


Now we can apply the same knowledge here


     const sequence = (funcs: AsyncCallbackFn[]): AsyncCallbackFn => {
        return function(callback, initialData) {
             // Promisify each function in the array
            const promisifiedFns = funcs.map((fn) => promisify(fn));

            // Initialize the promise with the initialData
            const initPromise = Promise.resolve(initialData);

            // Chain the promises together to execute them sequentially
            const chainedPromise = promisifiedFns.reduce(async (accumulatorPromise, currentPromise) => {
                // Wait for the previous promise to resolve and get its value
                const value = await accumulatorPromise;

                // Execute the current promise with the value and return the result
                return await currentPromise(value);
            }, initPromise);

            // Handle the final result or error of the chained promises
            chainedPromise.then((data) => {
                callback(null, data);
            }).catch((error) => {
                callback(error, undefined);
            })
        }
     }

Let's try with reduce and Promises


     const sequence = (funcs: AsyncCallbackFn[]): AsyncCallbackFn => {
        return function(callback, initialData) {

            // Promisify each function in the array
            const promisifiedFns = funcs.map((fn) => promisify(fn));

            // Initialize the promise with the initialData
            const initPromise = Promise.resolve(initialData);

           // Chain the promises together to execute them sequentially
           const chainedPromises = promisifiedFns.reduce<Promise<number>>((accumulatorPromise, currentPromise) => {
                // Wait for the accumulatorPromise to resolve and pass the result to the currentPromise
                return accumulatorPromise.then((data) => currentPromise(data));
           }, initPromise);

           // Handle the final result or error of the chained promises
           chainedPromises.then((data) => {
                callback(null, data);
           }).catch((error) => {
                callback(error, undefined);
           })
        }
     }

Tadaa! 🎊