How to await inside setInterval in JS?

26,758

Solution 1

Turn the interval function into a recursive setTimeout function instead, that way you can initialize a timeout for the next iteration once the function has finished.

async function doScroll() {
  window.scrollBy(0, scrollHeight);
  const scrollTop = document.documentElement.scrollTop;
  let lastDate = null;
  if (maxDate) {
    const html = new XMLSerializer().serializeToString(document.doctype) + document.documentElement.outerHTML;
    await extractDate(html).then((date) => {
      lastDate = date;
    });
  }
  if (scrollTop === lastScrollTop ||
      (maxDate && lastDate && maxDate.getTime() >= lastDate.getTime())) {
    // No need to `clearInterval`:
    resolve();
  } else {
    lastScrollTop = scrollTop;
    // Recursive setTimeout:
    setTimeout(doScroll, 2000); // <------------------
  }
}
setTimeout(doScroll, 2000);

Solution 2

Use the following code:

setInterval(async () => {
    await fetch("https://www.google.com/") 
}, 100);

Solution 3

I generally opt for this solution. I think it's cleaner:

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function loop() {
  while (/* condition */) {
    /* code to wait on goes here (sync or async) */    

    await delay(100)
  }
}

Your loop function will return a promise. You can wait for it to stop looping, or you can discard it.

Solution 4

Make the interval a function instead and use setTimeout to queue the future function call.

const interval = async function () { // instead of setInterval

Then use setTimeout function where you want to to queue the future call:

setTimeout(interval, 2000);

Fiddle example: http://jsfiddle.net/t9apy3ec/5/

Share:
26,758
tinker
Author by

tinker

Updated on July 04, 2020

Comments

  • tinker
    tinker almost 4 years

    I have a code segment that looks like this:

    async function autoScroll(page, maxDate = null) {
      await page.evaluate(async () => {
        await new Promise(async (resolve, reject) => {
            try {
                const scrollHeight = document.body.scrollHeight;
                let lastScrollTop = 0;
    
                const interval = setInterval(async () => {
                    window.scrollBy(0, scrollHeight);
                    const scrollTop = document.documentElement.scrollTop;
                    let lastDate = null;
    
                    if (maxDate) {
                        const html = new XMLSerializer().serializeToString(document.doctype) + document.documentElement.outerHTML;
    
                        await extractDate(html).then((date) => {
                            lastDate = date;
                        });
                    }
    
                    if (scrollTop === lastScrollTop || 
                        (maxDate && lastDate && maxDate.getTime() >= lastDate.getTime())) {
                        clearInterval(interval);
                        resolve();
                    } else {
                        lastScrollTop = scrollTop;
                    }
                }, 2000);
            } catch (err) {
                console.error(err);
                reject(err.toString());
            }
        });
    });
    }
    

    Where extractDate method has the following form:

    function extractDate(html) {
        return new Promise((resolve, reject) => {
            // Rest removed for brevity.
            resolve(result);
        });
    }
    

    Now the problem is that, my code keeps scrolling, but it doesn't wait for the other stuff inside setInterval to finish, as it keeps scrolling every 2 seconds, but normally extractDate function should take longer than 2 seconds, so I actually want to await for everything inside setInterval to finish before making the call to the new interval.

    Because of the async nature of stuff, I didn't manage to console.log stuff so see the behavior of the code.

    So, how can I make sure that everything inside setInterval finishes before making the next interval call?

    EDIT:

    This solution using setTimeout scrolls just once and throws unhandled promise rejection error with puppeteer.

     async function autoScroll(page, maxDate = null) {
         await page.evaluate(async () => {
            await new Promise(async (resolve, reject) => {
                try {
                   const scrollHeight = document.body.scrollHeight;
                   let lastScrollTop = 0;
    
                    const interval = async function() {
                        window.scrollBy(0, scrollHeight);
                        const scrollTop = document.documentElement.scrollTop;
                        let lastDate = null;
    
                        if (maxDate) {
                            const html = new XMLSerializer().serializeToString(document.doctype) + document.documentElement.outerHTML;
                            await extractDate(html).then((date) => {
                                lastDate = date;
                            });
                        }
    
                        if (scrollTop === lastScrollTop || 
                           (maxDate && lastDate && maxDate.getTime() >= lastDate.getTime())) {
                            resolve();
                        } else {
                            lastScrollTop = scrollTop;
                            setTimeout(interval, 2000);
                        }
                    }
    
                    setTimeout(interval, 2000);
    
                } catch (err) {
                    console.error(err);
                    reject(err.toString());
                }
            });
        });
    }
    
  • CertainPerformance
    CertainPerformance over 5 years
    You would put all that inside the try block - everything else would be the same
  • tinker
    tinker over 5 years
    Please check my edited code with your solution. I tried that but using that approach it just scrolls once. No matter whether I comment out the if statement that includes the await call. Where previously it was scrolling all the time until if condition was satisfied.
  • Gustav G
    Gustav G over 5 years
    Try moving the interval function to the top of the parent promise function, and move the try catch block inside the interval. And at the promise line, try returning the promise and remove the async keyword from the promise function.
  • tinker
    tinker over 5 years
    Can you write it here as an answer.
  • president
    president over 4 years
    Thank you! I was searching for this solution very long time)
  • Admin
    Admin about 4 years
    No one asked about react
  • genuinefafa
    genuinefafa about 4 years
    beware of github.com/typescript-eslint/typescript-eslint/blob/v2.28.0/‌​… setInterval will receive a promise which it doesn't understand
  • Nir O.
    Nir O. almost 4 years
    what is this resolve() call ? it is not defined in the block. also, shouldn't it be scroll() instead of scroll ?
  • CertainPerformance
    CertainPerformance almost 4 years
    @NirO. This is what the code inside the try block should be. resolve is defined outside, in await new Promise(async (resolve, reject) => {
  • Robert Hollon
    Robert Hollon over 3 years
    This is absolutely smooth. I'm transitions my calls out of mssqlserver into node, and this resolves the logic steps that functions posted by others fail to account for: fully complete the statement before initiating any delay.
  • Yuriy Galanter
    Yuriy Galanter about 3 years
    This is beautiful
  • Rip3rs
    Rip3rs over 2 years
    Slick! thanks for this!
  • Rip3rs
    Rip3rs over 2 years
    Usually is not a good thing to mix await and then syntaxes as it will create more confusion.
  • Murrah
    Murrah over 2 years
    Brilliant solution. Thanks!