Home > Back-end >  async function does update iterator correctly at the begining but not at the end
async function does update iterator correctly at the begining but not at the end

Time:04-18

In the code bellow I work with CSS DOM, which may be computation heavy. This is apparently necessary to access the :after selector. renderItem method will add a new item to the DOM (including it's after element) and this is the reason why I have used the async function and await for it's return in each iteration inside of loadFromStorage.

However, the await seems to not work correctly, or something weird happens inside of renderItem function. The n iterator is updated correctly at the beginning of the function (items are correctly rendered to the screen and the first console.debug prints a correct value in a correct order), but the at the bottom, the second printed value, is always the last iteration value (which is 4 in my case, as I am trying to render 4 items from the local storage) and getCSSRule method is getting a wrong number.

let books = []
let n = 0

const renderItem = async (entry, direction = 1) => {
  const li = document.createElement('li')
  const ul = document.querySelector('ul')
  li.classList.add('item')
  n  = 1
  console.debug(`iter: ${n}`)
  li.id = (`item${n}`)
  await addCSSRule(`#item${n}:after`)
  li.innerText = entry.slice(0, entry.length - 13)
  if (direction === 1)
    ul.appendChild(li)
  else
    ul.insertBefore(li, ul.firstChild)
  console.debug(`iter: ${n}`)
  const s = await getCSSRule(`#item${n}::after`).catch(() => {
    console.debug(`Failed to find ':after' selector of 'item${n}'`)
    return false
  })
  s.style.content = "\""  entry.slice(entry.length - 13, entry.length)  "\""
  return true
}

const loadFromStorage = () => {
  books = localStorage.getItem('books').split('//')
  books.forEach(async (entry) => {
    await renderItem(entry)
  })
}

...

Console result (considering localStorage.getItem('books').split('//') returns 4 items):

iter: 1
iter: 2
iter: 3
iter: 4
iter: 4 // Printed x4

I been also trying to pass this renderItem method to await inside of a Promise object, which give me the same result. Also when I update the n iterator at the end of function the same thing happens, but at the beginning of it.

I am sorry if some terminology I have used is not correct in the context of JavaScript, I been not using this language for many years and currently I am trying to catch on.

CodePudding user response:

The key problem here is that you're passing an async function to forEach, so even though you're awaiting inside the body of it, forEach will not wait for the function itself. To illustrate the order of events here, say you have 4 books A, B, C, D. Your execution will look something like this.

  • renderItem(A)
  • n = 1 (n is now 1)
  • console.log(n) (logs 1)
  • await addCSSRule(`#item${1}:after`) (This is a truly asynchronous event, and so this frees up the event loop to work on other things, namely the next elements in the forEach)
  • renderItem(B)
  • n = 1 (2)
  • console.log(n) (logs 2)
  • ...
  • renderItem(C) ... n = 1 (3) ... await addCSSRule
  • renderItem(D) ... n = 1 (4) ... await addCSSRule

And then whenever the addCSSRule calls resolve n will always be 4 no matter which call you're in.

Solution

Use a for await...of loop instead of Array.prototype.forEach.

for await (const entry of books) {
    await renderItem(entry);
}

Or a traditional for loop, and modify renderItem to take n as an argument

for (let i = 0; i < books.length; i  ) {
    renderItem(books[i], i 1);
    // we don't need to await in this case, and we add 1 to i so that the 'n' value is 1-indexed to match your current behaviour.
}

I would prefer the latter option as it's best practice to avoid mutable global state (your n variable) - as it can lead to confusing control flow and issues just like the one you're having.

One other option is to set a local variable to the value of n after incrementing it inside renderItem, so that for the duration of that function the value won't change, but that seems like a very hacky workaround to me.

  • Related