0

I'm working on retrying some async calls in JS. When simplified and rewritten to setTimeout the logic looks like this:

let error = true

let promise = null

const runPromise = (value) => new Promise((res) => {
    if (!error) {
        res()
        return
    }

    if (promise) {
        return promise.then(() => {
            return runPromise(value) 
        })
    }

    promise = new Promise((res2) => {
        setTimeout(() => {
            promise = null
            console.log(value)
            error = false
            res2()
        }, 1000)
    }).then(() => res())
})

runPromise(1).then(() => { console.log(1) })
runPromise(2).then(() => { console.log(2) })
runPromise(3).then(() => { console.log(3) })

Why then blocks for runPromise(2) and runPromise(3) never got called?

Kobe
  • 6,226
  • 1
  • 14
  • 35
Vladyslav Zavalykhatko
  • 15,202
  • 8
  • 65
  • 100

2 Answers2

2

Your problem is that in the if (promise) case, the promise returned by runPromise is never res()olved. returning from the executor callback doesn't do anything. You could fix this by doing

const runPromise = (value) => new Promise((res) => {
    if (!error) {
        console.log("resolve immediately without error")
        res()
    } else if (promise) {
        promise.then(() => {
            console.log("resolve after waiting for previous promise")
            res(runPromise(value))
        })
    } else {
        promise = new Promise((res2) => {
            setTimeout(() => {
                promise = null
                error = false
                res2()
                console.log("resolve after timeout")
                res()
            }, 1000)
        })
    }
})

but really you should just avoid the Promise constructor antipattern, which caused this mistake in the first place. Don't call then, new Promise or runPromise() inside that outer new Promise executor! Instead use

let error = true
let promise = null

function runPromise(value) {
    if (!error) {
        console.log(value, "resolve immediately without error")
        return Promise.resolve();
    } else if (promise) {
        console.log(value, "defer until promise")
        // now this `return` works as expected
        return promise.then(() => {
            console.log(value, "trying again")
            return runPromise(value) 
        })
    } else {
        console.log(value, "starting timeout")
        promise = new Promise(res2 => {
            setTimeout(res2, 1000)
        }).then(() => {
            promise = null
            error = false
            console.log(value, "waited for timeout")
        });
        return promise;
    }
}

runPromise(1).then(() => { console.log(1) })
runPromise(2).then(() => { console.log(2) })
runPromise(3).then(() => { console.log(3) })
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • That polling is unnecessary though – Jonas Wilms Aug 15 '19 at 12:31
  • 1
    @JonasWilms Yes, I was just trying to show a cleaned up version that follows the same approach as the OP and fixed the error. Whether that is a (good) solution to the actual problem of the OP (or whether he would've needed a queue) I cannot say. – Bergi Aug 15 '19 at 12:37
0

You more or less have endless recursion here. Once you've set promise = the first time, if(promise) will always enter, and it will always attach another .then callback, that when called, just executes the same function again and again. It will also never call res().

I'd generally seperate concerns here: Have one function build up the promise chain and let it take a task function that gets called with the value provided:

 let queue = Promise.resolve(); // no need for null, let it always be a promise

 function runTask(task, ...args) {
  return queue = queue.then(() => task(...args));
 }

Then write another function, that represents a task, e.g. in your case it's a function that waits a tick:

 function waitTask(n) { return new Promise(res => setTimeout(res, 1000, n)); }

runTask(waitTask, 1);
runTask(waitTask, 2);

let queue = Promise.resolve();

function runTask(task, ...args) {
  return queue = queue.then(() => task(...args));
}


function waitTask(n) { return new Promise(res => setTimeout(res, 1000, n)); }

runTask(waitTask, 1).then(console.log);
runTask(waitTask, 2).then(console.log);
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • OP also sets `error = false` though which will make the function take the first branch, not the `if (promise)` one – Bergi Aug 15 '19 at 12:27