Search code examples
javascriptasync-awaitpromisescrollsettimeout

setTimeout is not honored when resolving callback function in a Promise


I have a promised based function in javascript that scrolls the web page incrementally, then scroll up to the top of the page after a brief period that uses setTimeout to delay calling callback function but it seems it is being resolved immediately.

The goal is to delay the callback until the page has scrolled at the top.

Here is the code I have:

// main scroll function
async function promiseScroll(callback = null) {
    return new Promise((resolve, reject) => {
        function pageScroll() {
            const scrollIncrement = document.body.offsetHeight / 5

            window.scrollBy({
                top: scrollIncrement,
                left: 0,
                behavior: "smooth"
            })

            let scrollDelay = setTimeout(pageScroll, 1000)

            if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) {
                clearTimeout(scrollDelay)
                setTimeout(resolve(promisePageUp(callback)), 2000)
            }
        }

        pageScroll()
    })
}

// scroll the page to top
async function promisePageUp(callback = null) {
    return new Promise((resolve, reject) => {
        window.scrollTo({
            top: 0,
            left: 0,
            behavior: "smooth"
        })
        // delayedCallback might not be needed
        // the invocation of callback is not delayed for some reason
        setTimeout(resolve(delayedCallback(callback)), 2000)
    })
}

// delay resolving of callback but this does not work
async function delayedCallback(callback = null) {
    return new Promise((resolve, reject) => {
        // expect the callback to be called after 2 seconds
        // issue might be because of `callback()`, but we cannot do `callback` either
        setTimeout(resolve(callback()), 2000)
    })
}

// sample callback for testing
async function callback() {
    return "callback is called!"
}

// Run promised based scroll simulation
// This is a async IIFE
(async () => {
    const value = await promiseScroll(callback)
    console.log(value)
})()

I might be missing something simple but I can't wrap my head around this and been fiddling since yesterday. Any advice would be appreciated.

There are related questions about Promise and setTimeout but I haven't found similar issue that answers the case I have.

Tried to do resolve(callback) but of course the callback would not be invoked and will return the function itself.


Solution

  • This line of code:

    setTimeout(resolve(promisePageUp(callback)), 2000)
    

    is calling promisePageUp(callback) immediately. The Javascript interpreter runs resolve(promisePageUp(callback)) immediately and passes the return result from that to setTimeout() which is not what you want. Instead, you want to pass a function reference to setTimeout() like this:

    setTimeout(() => resolve(promisePageUp(callback)), 2000)
    

    so that setTimeout() can call that function LATER when the timer fires.

    And, same issue with this which needs the same fix:

    setTimeout(resolve(callback()), 2000)
    

    Note that this one:

    let scrollDelay = setTimeout(pageScroll, 1000)
    

    is correct.


    FYI, your mixing of promises and callbacks generally leads to confusing and hard to maintain code. It would be better to promisify the lower levels and do all your code flow and logic using only promises. This includes even using a promisified version of setTimeout() which exists in the newest nodejs versions or it's trivial to make your own promsified version.