Egor Panok

Full Stack JavaScript Developer

Getting a loop counter from an async handler

September 05, 2018 - 4 min read

Getting a loop counter from an async handler

Suppose, we have asynchronous calls within a for loop. And then within the async handlers we need to get the counter / index of the loop. How we can accomplish this?

Let’s examine the following example (for the sake of simplicity here the asynchrony is simulated by setTimeout function).

let i

for (i = 0; i < 25; i++) {
    setTimeout(() => {
        console.log(i)
    }, 100)
}

If you run this, you’ll have 25 printed 25 times in the console. It happens due to the nature of asynchronous operations, or, more precisely, when the first async function handler is invoked our counter = 25. And BTW, the counter even exceeded the expected limit.

Solution

One possible solution for this will be enclosing the async function in the IIFE function and passing the loop counter as a parameter to the IIFE and then use the IIFE argument to get the value of the loop counter at a moment when the async function was called.

let i

for (i = 0; i < 25; i++) {
    ;(j => {
        setTimeout(() => {
            console.log(j)
        }, 100)
    })(i)
}

Voila! Now the result will be as expected: 0, 1, 2, … , 24.

So why does this trick work? Actually we’re employing the power of closures. At each loop step i will be copied to j and then in the async handler we’ll get the j from the closure.

Real Example - Fetching Data from REST Endpoints

Endpoints Overview

Let’s take a closer to the real world example when we really need to fetch data from REST endpoints. We’ll use a very nice service that provides several mock endpoints - https://jsonplaceholder.typicode.com/.

We’ll use the following endpoints:

  1. https://jsonplaceholder.typicode.com/todos/{id} The endpoint returns the todo by it’s id:
{
  "userId": 1,
  "id": 5,
  "title": "laboriosam mollitia et enim quasi adipisci quia provident illum",
  "completed": false
}

Note that todo object contains the userId property which can be used to fetch the data about the user.

  1. https://jsonplaceholder.typicode.com/users/{id}
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

The task

So, our task is:

  1. Get several todos by its ids.
  2. Lookup for the user for each todo (by todo’s userId property).
  3. Complement each todo with the user property from the step 2.

Solution

Here is the function allowing us to get todos by ids:

const apiUrl = "https://jsonplaceholder.typicode.com"

function getToDos(ids) {
    return new Promise((resolve, reject) => {
        let i,
            counter = ids.length
        const todos = []

        for (i = 0; i < ids.length; i++) {
            fetch(`${apiUrl}/todos/${ids[i]}`)
                .then(response => response.json())
                .then(todo => {
                    todos.push(todo)

                    if (--counter === 0) return resolve(todos)
                })
        }
    })
}

Please note that we’re using fetch function to do http requests. Here we don’t use the IIFE trick because we don’t need to get the loop counter from the promise handler.

Here is the function that complements todos with the user property by fetching them from /users endpoint:

function populateTodosWithUsers(todos) {
    return new Promise((resolve, reject) => {
        let i,
            counter = todos.length

        for (i = 0; i < todos.length; i++) {
            ;(function(j) {
                fetch(`${apiUrl}/users/${todos[i].userId}`)
                    .then(response => response.json())
                    .then(user => {
                        todos[j].user = user

                        if (--counter === 0) return resolve(todos)
                    })
            })(i)
        }
    })
}

Please note that from the user’s promise handler we need to know for which todo the user’s fetching function was called. That’s why we’re wrapping the async function into IIFE to get the state of the loop counter (i => j) when the async function was called.

Alright, now we’re ready to use it.

const todoIds = [55, 20, 90, 91, 100, 88, 45, 80, 102, 200, 195] // todo ids

getToDos(todoIds)
    .then(todos => {
        return populateTodosWithUsers(todos)
    })
    .then(todos => {
        console.log(todos)
    })

Here it’s. Now we have fetched todos with contains user property containing all information about the todo’s user.

Conclusion

If you have a for loop containing async calls and within that async handlers you need to get the value of loop counter relevant for the moment of calling that async function, then wrap your async call into an IIFE and pass the loop counter as a parameter, then use the IIFE argument to get the loop counter. That simple.

Happy coding!


Written by Egor Panok, full stack JavaScript Developer who loves building useful things. Follow him on Twitter

© 2020, Egor Panok, Full Stack JavaScript Developer