David Limkys
David Limkys

Reputation: 5133

Will using setTimeout prevent the stack from growing?

Lets say I'm runnin some kind of long operation for a set of values.

The function that starts this operation is startNext()

and the very last line executed in it is to itself so its a recursive call like so:

function startNext(){
   var val = getNextValue()
   workOnValue(val)
      .then(doSomeMoreWork)
      .then(doMoreStuff)
      .then(moree)
      .then(startNext);
}

This will make the stack to grow as tail recursion does not work in JS (yet). Will changing the last line to:

.then(function(){setTimeout(startNext, 0)});

Work better? Will it not fill the stack beacuse it adds a new operation to the event loop ?

Upvotes: 4

Views: 434

Answers (2)

Guy
Guy

Reputation: 67340

Yes, setTimeout will prevent the stack from growing because the current function completes and the "recursive" call to itself gets placed on the event queue. It will also run slower because its not being directly called and instead processed via the queue.

To demonstrate and prove this try an experiment with Node.

Put the code sample below into a file and switch the simple flag towards the bottom. You will see that the recurseSimple function runs super fast and blows the stack very quickly. The recurseTimeout runs slower but will run forever.

function recurseSimple(count) {
// Count: 15269, error: bootstrap_node.js:392
// RangeError: Maximum call stack size exceeded
  try {
    if (count % 10000 === 0) {
      console.log('Running count:', count);
    }
    recurseSimple(count + 1);
  } catch (e) {
    console.log(`Simple count: ${count}, error:`, e);
  }
}

function recurseTimeout(count) {
  // No stack exceeded
  try {
    if (count % 10000 === 0) {
      console.log('Running count:', count);
    }
    setTimeout(recurseTimeout.bind(null, count + 1), 0);
  } catch (e) {
    console.log(`Timeout count: ${count}, error:`, e);
  }
}

const simple = false;

if (simple) {
  recurseSimple(0);
} else {
  recurseTimeout(0);
}

Exactly the same principal applies to promises. I didn't use promises here in order to keep this as simple as possible.

Upvotes: 3

MinusFour
MinusFour

Reputation: 14423

then handlers are pushed out of the execution context stack, so it's already doing what you propose:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

This applies to A+ promises.

Here's the 3.1 note for clarity:

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

Promises A+

Upvotes: 1

Related Questions