24

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());
            }
        });
    });
}
tinker
  • 2,884
  • 8
  • 23
  • 35
  • What’s the error in the rejection? – Aankhen Aug 14 '18 at 11:40
  • @Aankhen It just scrolls once, and then after some time it throws `Error: protocol error (Runtime.callFunctionOn): Promise was collected`. Stack trace points to some puppeteer functions. – tinker Aug 14 '18 at 12:10

6 Answers6

37

Use the following code:

setInterval(async () => {
    await fetch("https://www.google.com/") 
}, 100);
ziishaned
  • 4,944
  • 3
  • 25
  • 32
  • 1
    Thank you! I was searching for this solution very long time) – president Dec 22 '19 at 21:11
  • 3
    beware of https://github.com/typescript-eslint/typescript-eslint/blob/v2.28.0/packages/eslint-plugin/docs/rules/no-misused-promises.md setInterval will receive a promise which it doesn't understand – genuinefafa Apr 30 '20 at 23:45
  • @genuinefafa `setInterval` will receive _a function_ that returns a promise, but it won't receive the promise itself. As far as I know, `setInterval` disregards the return values of the called function. – Sebastian May 23 '23 at 08:34
21

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.

Will Brickner
  • 824
  • 11
  • 12
14

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);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    You would put all that inside the `try` block - everything else would be the same – CertainPerformance Aug 13 '18 at 20:55
  • 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. – tinker Aug 13 '18 at 21:21
  • what is this resolve() call ? it is not defined in the block. also, shouldn't it be scroll() instead of scroll ? – Nir O. Jun 28 '20 at 13:43
  • @NirO. This is what the code inside the `try` block should be. `resolve` is defined outside, in `await new Promise(async (resolve, reject) => {` – CertainPerformance Jun 28 '20 at 14:56
6

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/

Gustav G
  • 459
  • 3
  • 10
  • 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. – Gustav G Aug 14 '18 at 12:32
  • Can you write it here as an answer. – tinker Aug 14 '18 at 14:31
0

If someone wants an updated solution there is react-timeout which cancels any lingering timers automatically when wrapped component is unmounted.

More info

https://www.npmjs.com/package/react-timeout

npm i react-timeout

While using await you can do something like this

  handleClick = async() => {
    try {
      await this.props.getListAction().then(()=>{
        this.timeOut = this.props.setTimeOut(this.handleClick, 10000);
      });

    } catch(err) {
      clearTimeout(this.timeOut);
    }
  }
Kodin
  • 772
  • 8
  • 23
  • Usually is not a good thing to mix `await` and `then` syntaxes as it will create more confusion. – Rip3rs Oct 15 '21 at 08:43
0

When using an async function as an argument to setInterval or setTimeout, make sure you wrap any operations that can throw errors in a try/catch to avoid un-handled errors and, on Node.js, do not call process.exit as it will pre-empt the event loop without any respect for timers. More detail in Async, Promises, and Timers in Node.js.

David Dooling
  • 674
  • 7
  • 6