5

I'm trying to convert some of my code to promises, but I can't figure out how to chain a new promise inside a promise.

My promise function should check the content of an array every second or so, and if there is any item inside it should resolve. Otherwise it should wait 1s and check again and so on.

function get(){
    return new Promise((resolve) => {

      if(c.length > 0){
        resolve(c.shift());

      }else{
        setTimeout(get.bind(this), 1000);
      }

    });

}


let c = [];

setTimeout(function(){
  c.push('test');
}, 2000);

This is how I expect my get() promise function to work, it should print "test" after 2 or 3 seconds max:

get().then((value) => {
  console.log(value);
});

Obviously it doesn't work, nothing is ever printed

thelolcat
  • 10,995
  • 21
  • 60
  • 102

6 Answers6

4

setTimeout has terrible chaining and error-handling characteristics on its own, so always wrap it:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

function get(c) {
  if (c.length) {
    return Promise.resolve(c.shift());
  }
  return wait(1000).then(() => get(c)); // try again
}

let c = [];
get(c).then(val => console.log(val));
wait(2000).then(() => c.push('test'));

While you didn't ask, for the benefit of others, this is a great case where async/await shines:

const wait = ms => new Promise(r => setTimeout(r, ms));

async function get(c) {
  while (!c.length) {
    await wait(1000);
  }
  return c.shift();
}

let c = [];
get(c).then(val => console.log(val));
wait(2000).then(() => c.push('test'));

Note how we didn't need Promise.resolve() this time, since async functions do this implicitly.

jib
  • 40,579
  • 17
  • 100
  • 158
2

The problem is that your recursive call doesn't pass the resolve function along, so the else branch can never call resolve.

One way to fix this would be to create a closure inside the promise's callback so that the recursive call will have access to the same resolve variable as the initial call to get.

function get() {
  return new Promise((resolve) => {
    function loop() {
      if (c.length > 0) {
        resolve(c.shift());
      } else {
        setTimeout(loop, 1000);
      }
    }
    loop();
  });
}

let c = [];
setTimeout(function() {
  c.push('test');
}, 2000);
get().then(val => console.log(val));
4castle
  • 32,613
  • 11
  • 69
  • 106
  • What happens is `c` isn't empty? I don't think what he wants is possible from a single Promise. – Mark Oct 29 '17 at 18:49
  • @Mark_M If `c` isn't empty, it will immediately `resolve` with the first value in `c`. – 4castle Oct 29 '17 at 18:50
  • The way I read the question was that he wants to continue checking after that. – Mark Oct 29 '17 at 18:50
  • @Mark_M What part of the question makes you think that? A promise can only resolve one time, so any subsequent calls to `resolve` would be ignored. – 4castle Oct 29 '17 at 18:52
  • Yes, I understand, that's why I thought it wasn't possible. I suppose the reason I read it this ways that there's little reason to go through all this to just return a single value … and also the '…and so on'. I certainly could have misread it. – Mark Oct 29 '17 at 18:54
0

In the else case, you never resolve that promise. get might create another one, but it is returned to nowhere.

You should promisify your asynchronous function (setTimeout) on the lowest level, and then only chain your promises. By returning the result of the recursive call from a then callback, the resulting promise will resolve with the same result:

function delayAsync(time) {
    return new Promise(resolve => {
        setTimeout(resolve, time);
    });
}
function get(c) {
    if (c.length > 0){
        return Promise.resolve(c.shift());
    } else {
        return delay(1000).then(() => {
            return get(c); // try again
        });
    }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

What you need is a polling service, which checks periodically for specific condition prior proceeding with promise resolution. Currently when you run setTimeout(get.bind(this), 1000); you are creating a new instance of the promise without actually resolving the initial promise, because you don't reference to the initial resolve function that you created.

Solution:

  • Create a new callback function that you can reference to it inside the promise
  • Pass the resolve & reject as params in the setTimeout invocation e.g. setTimeout(HandlePromise, 1000, resolve, reject, param3, param4 ..); setTimeout API

function get() {
  var handlerFunction = resolve => {
    if (c.length > 0) {
      resolve(c.shift());
    } else {
      setTimeout(handlerFunction, 1000, resolve);
    }
  };

  return new Promise(handlerFunction);
}

let c = [];

setTimeout(function() {
  c.push("test");
}, 2000);

get().then(value => {
  console.log(value);
});
KhaledMohamedP
  • 5,000
  • 3
  • 28
  • 26
0

You could try this solution. Since JS needs to free itself to download the images, I use await within an asynchronous function and an asynchronous call to wake up JS after a delay

private async onBeforeDoingSomething() : void {
    await this.delay(1000);
    console.log("All images are loaded");    
} 

private delay (ms : number = 500) : Promise<number> {
    return new Promise((resolve,reject) => {
        const t = setTimeout( () => this.areImgsLoaded(resolve), ms);
    });
} 

private async areImgsLoaded (resolve) {
    let reload = false;
    const img = document.querySelectorAll('img');
    console.log("total of images: ",img.length);

    for (let i = 0; i < img.length; i++){
        if (!img[i]["complete"]) {
            console.log("img not load yet");
            reload = true;
            break;
        }
    }

    if (reload) {
        await this.delay();
    }
    resolve();
}
-1

Use setInterval to check every second. Run this script to understand.

let c = [];

function get(){
    return new Promise((resolve) => {

      var i = setInterval(function(){
        if(c.length > 0){
          resolve(c.shift());
          clearInterval(i);
        }
      }, 1000);

    });
}



setTimeout(function(){
  c.push('test');
}, 2000);

get().then((value) => {
  console.log(value);
});
Abhinav Jain
  • 329
  • 1
  • 2
  • 9
  • You should also provide a way to remove interval once it is resolved. Otherwise, the callback will keep running infinitely. – Prakash Sharma Oct 29 '17 at 18:54