Thursday, 30 July 2026

AsyncLoop 2026

With "async loop" in this post I'm not talking about something like JavaScript's for-await or Python's async-for, where the iterator is asynchronous. What I'm talking about here is about performing an asynchronous operation in a loop (like doing a loop of http requests). Well, in the async/away world that's not something to be scared of, but in the past, when asynchronous code was based on callbacks (not even Promises) this was a bit more complicated. 15 years ago I published a couple of posts about that [1] and [2]. For whatever the reason recently my mind came to think a bit about that old stuff, and I've decided to do a sort of generic AsyncLoop class that could be used with callback based functions. It's intended for use as a sort of for-of loop (so for iterator based loops) and for normal for loops (with a specific logic for getting the next item and checking for stop).

The AsyncLoop class receives a function to obtain the next item in the iteration, a function to check if the iteration must finish and a function (actionFn) for the body of the loop. That function is the one that behaves asynchronously and expects a callback. The class provides to the action/body a callback that will take care of continuing (or stopping) with the next iteration.


class AsyncLoop {
    constructor(nextItemFn, conditionFn, actionFn, onEndFn) {
        this.nextItemFn = nextItemFn; // function that returns the next item
        this.conditionFn = conditionFn; // function that receives the current item 
        this.actionFn = actionFn; // function that receives the current item, a callback to invoke when done
        this.onEndFn = onEndFn; // function that is invoked when the loop ends
    }

    run() {
        let item = this.nextItemFn();
        if (this.conditionFn(item)) {
            this.actionFn(item, () => {
                    this.run();
                }
            );
        } 
        else {
            this.onEndFn();
        }
    }
}


We can use it like this:


// callback based asynchronous function
function getContent(item, callback) {
    setTimeout(() => {
        let content = "Content for: " + item;
        callback(content);
    }, 500);
}

function testAsyncLoop() {
    let countriesIter = ["France", "Germany", "Italy"].values();
    new AsyncLoop(
        () => countriesIter.next().value, // nextItemFn
        (item) => item !== undefined, // conditionFn
        // actionFn
        (item, nextFn) => {
            getContent(item, result => {
                console.log(result);
                nextFn();
            }); 
        },
        () => console.log("All items processed") // onEndFn
    ).run();
}

We can use it also in nested loops:


class Continent {
    constructor(name, countries) {
        this.name = name;
        this.countries = countries;
    }   
}

function testNestedAsyncLoop() {
    let continentsIter = [
        new Continent("Europe", ["France", "Germany", "Italy"]),
        new Continent("Asia", ["China", "Japan", "India"]),
        new Continent("Africa", ["Nigeria", "Egypt", "South Africa"])
    ].values();

    //nested loop
    new AsyncLoop(
        () => continentsIter.next().value, // nextItemFn
        (continent) => continent !== undefined, // conditionFn
        (continent, nextContinentFn) => {
            console.log("Processing continent: " + continent.name);
            let countriesIter = continent.countries.values();
            new AsyncLoop(
                () => countriesIter.next().value,
                (country) => country !== undefined,
                (country, nextCountryFn) => {
                    getContent(country, result => {
                        console.log(result);
                        nextCountryFn();
                    });
                },
                () => nextContinentFn() // onEndFn
            ).run();
        },
        () => console.log("All continents processed")
    ).run();
}

Async/await has always felt a bit like magic (all what the compiler does under the covers), but looking to the above code that we would have written in 2011 and comparing it to how we would write it now with async/await is like a quantum leap!


function getContentPromisified(item) {
    return new Promise((res, rej) => getContent(item, res));
}

async function testUsingAsyncAwait(){
    let continents = [
        new Continent("Europe", ["France", "Germany", "Italy"]),
        new Continent("Asia", ["China", "Japan", "India"]),
        new Continent("Africa", ["Nigeria", "Egypt", "South Africa"])
    ];
    for (let continent of continents) {
        console.log("Processing continent: " + continent.name);
        for (let country of continent.countries) {  
            console.log(await getContentPromisified(country));  
        }
    }
    console.log("All continents processed");
}

No comments:

Post a Comment