5

I'm playing around with promises and I'm having trouble with an asynchronous recursive promise.

The scenario is an athlete starts running the 100m, I need to periodically check to see if they have finished and once they have finished, print their time.

Edit to clarify :

In the real world the athlete is running on a server. startRunning involves making an ajax call to the server. checkIsFinished also involves making an ajax call to the server. The code below is an attempt to imitate that. The times and distances in the code are hardcoded in an attempt to keep things as simple as possible. Apologies for not being clearer.

End edit

I'd like to be able to write the following

startRunning()
  .then(checkIsFinished)
  .then(printTime)
  .catch(handleError)

where

var intervalID;
var startRunning = function () {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0
  };
  var updateAthlete = function () {
    athlete.distanceTravelled += 25;
    athlete.timeTaken += 2.5;
    console.log("updated athlete", athlete)
  }

  intervalID = setInterval(updateAthlete, 2500);

  return new Promise(function (resolve, reject) {
    setTimeout(resolve.bind(null, athlete), 2000);
  })
};

var checkIsFinished = function (athlete) {
  return new Promise(function (resolve, reject) {
    if (athlete.distanceTravelled >= 100) {
      clearInterval(intervalID);
      console.log("finished");
      resolve(athlete);

    } else {
      console.log("not finished yet, check again in a bit");
      setTimeout(checkIsFinished.bind(null, athlete), 1000);
    }    
  });
};

var printTime = function (athlete) {
  console.log('printing time', athlete.timeTaken);
};

var handleError = function (e) { console.log(e); };

I can see that the promise that is created the first time checkIsFinished is never resolved. How can I ensure that that promise is resolved so that printTime is called?

Instead of

resolve(athlete);

I could do

Promise.resolve(athlete).then(printTime);

But I'd like to avoid that if possible, I'd really like to be able to write

startRunning()
  .then(checkIsFinished)
  .then(printTime)
  .catch(handleError)
user5325596
  • 2,310
  • 4
  • 25
  • 42
  • 3
    This code is really hard to follow, but it appears that it's making a really wrong use of promises. Unless I completely misunderstood this, I think you should take a few moments to think about this design again, maybe reading about promises first. – Amit Sep 11 '15 at 14:17
  • You need to understand that each `.then()` gets executed only once, and promises get resolved or reject once and for eternity. So, the first time you reach `checkIsFinished`, you return a promise which you leave pending and then recursively call yourself again using `setTimeout`. But that new promise goes nowhere, as `setTimeout` just returns a handle, not a promise and so this new promise also stays pending, then you repeat - probably causing a memory leak. Take the time to read up on and understand the subtleties of Javascript's event loop. It'll save you lots of time and frustration. – caasjj Sep 11 '15 at 17:42
  • when you do `setTimeout(checkIsFinished.bind(null, athlete), 1000);` you create inside a **new** Promise, so old promise never solved – Grundy Sep 12 '15 at 07:09
  • @Amit, I've edited my question to clarify the situation. – user5325596 Sep 12 '15 at 11:11
  • @caasjj, @Grundy, thanks for the comments. I understand that `setTimeout(checkIsFinished.bind(null, athlete), 1000)` creates a new promise and the initial promise gets "lost". My question is basically is there a way to get around that? I've edited the question in an attempt to clarify the situation. – user5325596 Sep 12 '15 at 11:14

3 Answers3

9

The bug is that you are passing a function that returns a promise to setTimeout. That promise is lost into the ether. A band-aid fix might be to recurse on the executor function:

var checkIsFinished = function (athlete) {
  return new Promise(function executor(resolve) {
    if (athlete.distanceTravelled >= 100) {
      clearInterval(intervalID);
      console.log("finished");
      resolve(athlete);
    } else {
      console.log("not finished yet, check again in a bit");
      setTimeout(executor.bind(null, resolve), 1000);
    }    
  });
};

But meh. I think this is a great example of why one should avoid the promise-constructor anti-pattern (because mixing promise code and non-promise code inevitably leads to bugs like this).

Best practices I follow to avoid such bugs:

  1. Only deal with async functions that return promises.
  2. When one doesn't return a promise, wrap it with a promise constructor.
  3. Wrap it as narrowly (with as little code) as possible.
  4. Don't use the promise constructor for anything else.

After this, I find code easier to reason about and harder to bugger up, because everything follows the same pattern.

Applying this to your example got me here (I'm using es6 arrow functions for brevity. They work in Firefox and Chrome 45):

var console = { log: msg => div.innerHTML += msg + "<br>",
                error: e => console.log(e +", "+ e.lineNumber) };

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

var startRunning = () => {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0,
    intervalID: setInterval(() => {
      athlete.distanceTravelled += 25;
      athlete.timeTaken += 2.5;
      console.log("updated athlete ");
    }, 2500)
  };
  return wait(2000).then(() => athlete);
};

var checkIsFinished = athlete => {
  if (athlete.distanceTravelled < 100) {
    console.log("not finished yet, check again in a bit");
    return wait(1000).then(() => checkIsFinished(athlete));
  }
  clearInterval(athlete.intervalID);
  console.log("finished");
  return athlete;
};

startRunning()
  .then(checkIsFinished)
  .then(athlete => console.log('printing time: ' + athlete.timeTaken))
  .catch(console.error);
<div id="div"></div>

Note that checkIsFinished returns either athlete or a promise. This is fine here because .then functions automatically promote return values from functions you pass in to promises. If you'll be calling checkIsFinished in other contexts, you might want to do the promotion yourself, using return Promise.resolve(athlete); instead of return athlete;.

Edit in response to comments from Amit:

For a non-recursive answer, replace the entire checkIsFinished function with this helper:

var waitUntil = (func, ms) => new Promise((resolve, reject) => {
  var interval = setInterval(() => {
    try { func() && resolve(clearInterval(interval)); } catch (e) { reject(e); }
  }, ms);
});

and then do this:

var athlete;
startRunning()
  .then(result => (athlete = result))
  .then(() => waitUntil(() => athlete.distanceTravelled >= 100, 1000))
  .then(() => {
    console.log('finished. printing time: ' + athlete.timeTaken);
    clearInterval(athlete.intervalID);
  })
  .catch(console.error);

var console = { log: msg => div.innerHTML += msg + "<br>",
                error: e => console.log(e +", "+ e.lineNumber) };

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

var waitUntil = (func, ms) => new Promise((resolve, reject) => {
  var interval = setInterval(() => {
    try { func() && resolve(clearInterval(interval)); } catch (e) { reject(e); }
  }, ms);
});

var startRunning = () => {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0,
    intervalID: setInterval(() => {
      athlete.distanceTravelled += 25;
      athlete.timeTaken += 2.5;
      console.log("updated athlete ");
    }, 2500)
  };
  return wait(2000).then(() => athlete);
};

var athlete;
startRunning()
  .then(result => (athlete = result))
  .then(() => waitUntil(() => athlete.distanceTravelled >= 100, 1000))
  .then(() => {
    console.log('finished. printing time: ' + athlete.timeTaken);
    clearInterval(athlete.intervalID);
  })
  .catch(console.error);
<div id="div"></div>
Community
  • 1
  • 1
jib
  • 40,579
  • 17
  • 100
  • 158
  • 2
    While your "Best Practices" list looks competent, it lead you into a scenario where you recursively create new promises and instead of chaining, you're constructing a deeper & deeper "promise stack". Each `wait(1000).then(...)` is hosting a new Promise - akin to `wait(x).then(()=>wait(x).then(()=>wait(x).then(...)))`. This will have adverse effect on memory & performance if/when and end result arrives. What's worse is that all that details is hidden away in flashy syntax and abstraction. – Amit Sep 12 '15 at 22:23
  • @Amit you're quite wrong. Firstly, there is no "stack" recursion here, as things aren't added on the same run. There's no construction of a chain ahead of time. All that's happening is that every second, a new promise is created, *just like in your example*, and at that same moment, all references to the previous promise disappear, making it garbage collectable. This can go on forever without any memory issues. There is no "chain" constructed the way you're picturing it. My preferred solution here would use `setInterval` instead, but your question asked for "recursive" so there you go. – jib Sep 12 '15 at 22:50
  • Not sure what question of mine you're referring to, but the promises can't be collected because of what I explained in my previous comment. Every promise is resolved and then a `.then()` is called with a callback that returns a new promise. That then call will eventually resolve to whatever the internal promise resolves to, but until it does, it's there, and it's waiting. And that goes on and on, "recursively". The fact that things don't happen ahead of time is irrelevant since if the end event never arrives, you'll create promises till memory is out. – Amit Sep 12 '15 at 23:03
  • 1
    @Amit I'm quite convinced promise implementations can optimize this away, as touched on in [this answer to a related question](http://stackoverflow.com/a/29931657/918910), but there's no room to share my analysis here. However, I admit I don't know if any implementations are doing this, but I'll try to find out. - In spite of your question being titled "How to resolve recursive asynchronous promises?", I've edited my answer to provide a non-recursive answer that I hope will satisfy your concerns of memory usage. – jib Sep 13 '15 at 02:41
  • Once again, ***I didn't ask any question***, but I did point out a flaw in your design. The (excellent) link in your previous comment *proves* my point, I don't see how you confused that. You're edited solution looks better, leans towards ny own answer, and might be good - but at least to me it's quite confusing / unreadable (the `waitUntil` function), though it does make a very concise code in the `.then()` call. If that really works, I like it – Amit Sep 13 '15 at 05:38
  • 1
    My apologies @Amit, I had you confused with the OP, and you may be right about existing implementations. Thanks for your feedback, I've edited the waitUntil function to make it clearer. Just noticed your answer. – jib Sep 13 '15 at 12:02
  • @Amit: The "recursive" solution is totally fine - readable and understandable, it's the preferred way. If this code hogs memory and creates performance issues, that is a promise implementation bug. – Bergi Sep 13 '15 at 12:28
  • @Bergi I don't know about preferred, but it is what the OP asked about. I agree with Amit that polling should avoided, and the times I've needed it are rare. Still a valid illustration for a question though I think. – jib Sep 13 '15 at 12:42
  • @Bergi your own answer (linked by jib above) proves my performance issues point. I don't understand how you reject that claim. Regarding the unreadable part, that comment wasn't about the recursive solution, it was about the previous (now improved) non recursive solution. – Amit Sep 13 '15 at 13:51
  • @Amit: Yes, and as I wrote there, performance issues are a bug of the library, and can be fixed if they really appear. Recursion is still the better design imo. – Bergi Sep 13 '15 at 16:12
  • @Bergi. Thanks for your comments. – user5325596 Sep 14 '15 at 08:21
  • Out of the 3 solutions in this answer, the 1st & 3rd are not "safe" in terms of exception handling and the 2nd creates performance issues when using common implementations, including ES2015 (as I noted above). The unsafe part (1 & 3) comes from the fact that asynchronous exceptions are not caught by the Promise object and are not propagated to the eventual `.catch()` handler. The code that runs inside `.setTimeout()` / `.setInterval()` will behave this way. To see that, throw a `new Error()` before `resolve(athlete)` & inside `func()` (try `athlete.a()`) in solutions 1 & 3 respectively. – Amit Sep 16 '15 at 06:38
  • @Amit Good point about `setInterval`. This is exactly why I advocate the smallest possible wrappers, like `wait` around dangerous `setTimeout`. `setInterval` is trickier. I've updated the `waitUntil` function in the non-recursive (3rd) solution with try/catch like you show to propagate errors correctly. ES2015 is not an implementation. – jib Sep 17 '15 at 01:07
  • That's better. I would make one more change though... I'd restructure and rename `waitUntil` to be `pollResolveWhen`, and add a 3rd parameter to resolve with. Then the call would be `pollResolveWhen(() => athlete.distanceTravelled >= 100, 1000, athlete)` and you can eliminate the global and provide a solution that matches OP requirements. – Amit Sep 17 '15 at 12:28
  • @Amit - in your first commit you say that "Each `wait(1000).then(...)` is hosting a new Promise - akin to `wait(x).then(()=>wait(x).then(()=>wait(x).then(...))).`" Is it not more accurate to say that it's akin to `wait(x).then(()=>wait(x)).then(()=>wait(x)).then(...)`? – user5325596 Sep 18 '15 at 10:36
  • @user5325596 - Definitely not. That's the reason I posted that comment, because it does exactly what I pointed out. Just by looking at the code you can see that each `wait()` is **directly** followed by a `.then()`, that can't "jump out" to the outer promise. You can also try for yourself, attach another `then()` after the first one, and `console.log` something there. You'll see that all logs happen "at the same time" when the entire chain is resolved. You can safely use the last solution in this answer now - jib has fixed it - but it's not the pattern you asked for (mine is :-). – Amit Sep 18 '15 at 14:13
  • @user5325596 Both of your wait-analogies are flawed comparisons, as they build *promise-chains* i.e. chains of execution, whereas recursive `.then` use merely builds *resolve-chains* which [are different](http://stackoverflow.com/a/29931657/918910). @Bergi and I contend that resolve chains can and should be optimized, but Amit contends they aren't. This isn't proven by adding `.then(() => console.log())` in-between steps, because doing so alters things: They add code that must be executed all at once on resolve, which thwarts optimization by implementations. Observer's paradox. – jib Sep 18 '15 at 14:40
1

Using setTimeout / setInterval is one of the scenrios that doesn't play well with promises, and causes you to use the frowned promise anti-pattern.

Having said that, if you reconstruct your function make it a "wait for completion" type of function (and name it accordingly as well), you'd be able to solve your problem. The waitForFinish function is only called once, and returns a single promise (albeit a new one, on top of the original promise created in startRunning). The handling of the recurrence through setTimeout is done in an internal polling function, where proper try/catch is used to ensure exceptions are propagated to the promise.

var intervalID;
var startRunning = function () {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0
  };
  var updateAthlete = function () {
    athlete.distanceTravelled += 25;
    athlete.timeTaken += 2.5;
    console.log("updated athlete", athlete)
  }

  intervalID = setInterval(updateAthlete, 2500);

  return new Promise(function (resolve, reject) {
    setTimeout(resolve.bind(null, athlete), 2000);
  })
};

var waitForFinish = function (athlete) {
  return new Promise(function(resolve, reject) {
    (function pollFinished() {
      try{
        if (athlete.distanceTravelled >= 100) {
          clearInterval(intervalID);
          console.log("finished");
          resolve(athlete);
        } else {
          if(Date.now()%1000 < 250) { // This is here to show errors are cought
            throw new Error('some error');
          }
          console.log("not finished yet, check again in a bit");
          setTimeout(pollFinished, 1000);
        }
      }
      catch(e) { // When an error is cought, the promise is properly rejected
        // (Athlete will keep running though)
        reject(e);
      }
    })();
  });
};

var printTime = function (athlete) {
  console.log('printing time', athlete.timeTaken);
};

var handleError = function (e) { console.log('Handling error:', e); };

startRunning()
  .then(waitForFinish)
  .then(printTime)
  .catch(handleError);

While all this code is functioning properly, a polling solution is never advised in an asynchronous environment and should be avoided if possible. In your case, since this sample mocks communication with a server, I'd consider using web sockets if possible.

Amit
  • 45,440
  • 9
  • 78
  • 110
  • I agree that a socket based solution would be preferable. Unfortunately, that's not possible at the moment. – user5325596 Sep 14 '15 at 08:14
  • your solution doesn't seem to work. When i run the code snippet, I see multiple messages of the form `updated athlete Object { timeTaken: 2.5, distanceTravelled: 25 } ` in the console, but no `not finished yet` messages and no `finished` message. – user5325596 Sep 14 '15 at 08:26
  • @user5325596 - run it several times. I ***intentionally*** added a randomly exception throwing code that shows how this solution handles exceptions (which might happen in your real code). When such exception is thrown, you won't see `finished`. You should clean this code part for real-use. (The relevant line is `if(Date.now()%1000 < 250)`) – Amit Sep 14 '15 at 08:29
  • @user5325596 - BTW, you should know that at least from my understanding (I'm quite confident about it), the solutions in your current accepted answer all lack exception handling (try throwing inside them) or are creating memory / performance issues. You should consider the consequences of using them. – Amit Sep 14 '15 at 19:30
  • @Amit - Note that exceptions inside `.then` functions and promise constructor executor functions cause rejection automatically, so your exception-handling is redundant. The accepted answer has sufficient error handling in that any coding errors inside the synchronous part of `startRunning` will be thrown to console by JS, and the subsequent promise chain is properly terminated with a `.catch` that reports errors. The accepted answer also has a non-recursive solution without performance controversy. – jib Sep 16 '15 at 04:42
  • @jib - not only are you wrong, the exception-handling is ***crucial***. the Promise executor / `.then()` function only handle *SYNCHRONOUS* exceptions. Code within `.setTimeout()` / .`setInterval` that throws will not be caught by the promise and will not be propagated to the eventual `.catch()` handler. The fact that the engine will catch and report to console is irrelevant - that's why there's a call to `.catch()` in the first place. That's exactly the problem with your answer(s). – Amit Sep 16 '15 at 06:42
  • Thanks for the continued comments. I've been busy with other work but have some time now to experiment some more with this. – user5325596 Sep 16 '15 at 10:12
0

Since your use of promises is pretty off the mark, it's a little hard to tell exactly what you're trying to do or what implementation would best fit, but here's a recommendation.

Promises are a one-shot state machine. As such, you return a promise and exactly one time in the future, the promise can be either rejected with a reason or resolved with a value. Given that design of promises, I think what makes sense would be something that can be used like this:

startRunning(100).then(printTime, handleError);

You could implement that with code like this:

function startRunning(limit) {
    return new Promise(function (resolve, reject) {
        var timeStart = Date.now();
        var athlete = {
            timeTaken: 0,
            distanceTravelled: 0
        };
        function updateAthlete() {
            athlete.distanceTravelled += 25;
            console.log("updated athlete", athlete)
            if (athlete.distanceTravelled >= limit) {
                clearInterval(intervalID);
                athlete.timeTaken = Date.now() - timeStart;
                resolve(athlete);
            }
        }
        var intervalID = setInterval(updateAthlete, 2500);
    });
}

function printTime(athlete) {
    console.log('printing time', athlete.timeTaken);
}

function handleError(e) { 
    console.log(e); 
}

startRunning(100).then(printTime, handleError);

Working demo: http://jsfiddle.net/jfriend00/fbmbrc8s/


FYI, my design preference would probably be to have a public athlete object and then methods on that object to start running, stop running, etc...


Here are some of the fundamental things you got wrong in a use of promises:

  1. They are one-shot objects. They are resolved or rejected only once.
  2. The structure startRunning().then(checkIsFinished) just doesn't make logical sense. For the first part of this to work, startRunning() has to return a promise, and it has to resolve ore reject that promise when something useful happens. Your are just resolving it after two seconds which doesn't really seem to accomplish anything useful.
  3. The words of your description make it sound like you want `checkIsFinished() to keep going an not resolve its promise until the athlete is finished. It is possible to do that by continually chaining promises, but that seems a very complicated way to do things and certainly not necessary here. Further, that isn't at all what your code attempts to do. Your code just returns a new promise that is never resolved unless the athelete has already passed the desired distance. If not, it returns a promise that is never resolved or rejected. This is a fundamental violation of promise concepts. A function that returns a promise is responsible for eventually resolving or rejecting it unless the calling code expects to just abandon the promise in which case it's probably the wrong design tool.

Here's another approach that creates a public Athlete() object that has some methods and allows multiple people to be watching the progress:

var EventEmitter = require('events');

function Athlete() {
    // private instance variables
    var runInterval, startTime; 
    var watcher = new EventEmitter();

    // public instance variables
    this.timeTaken = 0;
    this.distanceTravelled = 0;
    this.startRunning = function() {
        startTime = Date.now();
        var self = this;
        if (runInterval) {clearInterval(runInterval);}
        runInterval = setInterval(function() {
            self.distanceTravelled += 25;
            self.timeTaken = Date.now() - startTime;
            console.log("distance = ", self.distanceTravelled);
            // notify watchers
            watcher.emit("distanceUpdate");
        },2500);
    }
    this.notify = function(limit) {
        var self = this;
        return new Promise(function(resolve, reject) {
            function update() {
                if (self.distanceTravelled >= limit) {
                    watcher.removeListener("distanceUpdate", update);
                    resolve(self);
                    // if no more watchers, then stop the running timer
                    if (watcher.listeners("distanceUpdate").length === 0) {
                        clearInterval(runInterval);
                    }
                }
            }
            watcher.on("distanceUpdate", update);
        });
    }
}

var a = new Athlete();
a.startRunning();
a.notify(100).then(function() {
    console.log("done");
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • thanks for answering. I've edited my question to make things clearer. The code as written is just an attempt to immitate the real-world scenario where the athlete is actually on a server. – user5325596 Sep 12 '15 at 11:16
  • @user5325596 - next time, please describe your ACTUAL situation so we don't waste so much time working on the wrong problem - quite frustrating! So, do you want `checkIsFinished()` to return a promise that is resolved when it is actually finished and that will be determined by polling your server internal to `checkIsFinished` with repeated ajax calls to see when it's actually done? – jfriend00 Sep 12 '15 at 20:38
  • @user5325596 - sorry, but far too little participation from you in this question (days between responses) for me to continue working on this further. – jfriend00 Sep 14 '15 at 23:16