9

I'm failing to figure out why calling recSetTimeOut() does not result in a stack overflow error, while recPromise() does.

const recSetTimeOut = () => {
  console.log('in recSetTimeOut');
  setTimeout(recSetTimeOut, 0)
};
recSetTimeOut();

const recPromise = () => {
  console.log('in recPromise');
  Promise.resolve().then(recPromise);
}
recPromise();

Why does it happen? What is the difference between them?

Can you explain the process behind the scene?


Edit with a bit more of information

Running this snippets on Node.js v12.1.0 and Chrome DevTools:

const recSetTimeOut = () => { setTimeout(recSetTimeOut, 0); }
recSetTimeOut();

Result Node: No error.

Result Chrome: No error.

const recPromise = () => { Promise.resolve().then(recPromise); }
recPromise();

Result Node:

FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory

Result Chrome: Browser crushes.

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • 2
    No recursion. The "recursive" `recSetTimeout` call is actually scheduled and isn't part of the current function stack. Every time the callback is made, the stack is clean, with only the scheduler at the top of the stack. –  Jul 08 '19 at 15:08
  • 1
    @Amy, neither is the Promise one, right? Promises are always async? – Ruan Mendes Jul 08 '19 at 15:09
  • 2
    You can verify this by emitting an exception and checking the stack trace. –  Jul 08 '19 at 15:09
  • I can run the second example without a stack overflow when I type it into the console – Ruan Mendes Jul 08 '19 at 15:31
  • @JuanMendes after sometime of continues execution it start blocking the browser, – Code Maniac Jul 08 '19 at 15:41
  • I think the second one fails because it never yields control back to the UI thread, while the first repeatedly yields control back. The first doesn't create a huge stack; each call is made by the scheduler. The second is effectively an infinite loop. –  Jul 08 '19 at 15:45
  • @Amy Sounds like an answer deserving an upvote... – Clint Jul 08 '19 at 15:46
  • When I run this in the Node debugger, the call stack doesn't fill up and I don't get a stack overflow. But the memory use quickly increases. I suspect in the case of Node the callback queue is just getting bigger and bigger with the nested `then`s. Eventually it will crash when it runs out of memory. – Mark Jul 08 '19 at 15:47
  • @Clint I guess I can submit that as an answer. –  Jul 08 '19 at 15:48
  • 2
    As noted, I don't see this generate a stack overflow at all, at any point. Things slow down, but that's because you're constantly running code, and the scheduler, GC, etc need to catch up. If you're seeing an _actual_ stack overflow message, please remember to add that to your question. If not: better to mention what you're seeing, rather than mentioning a stack overflow. – Mike 'Pomax' Kamermans Jul 08 '19 at 15:53
  • I also don't see the exception in either snippet. But I do notice that the `setTimeout` is WAY slower. So I'm wondering (for those who do get it) if it is just a matter of not waiting long enough. Does the amount that "in recSetTimeOut" is logged exceed the mount of times that "in recPromise" is logged? – Ivar Jul 08 '19 at 15:59
  • See also [Why the function called by setTimeout has no callstack limit?](https://stackoverflow.com/q/24631041/1048572), [Leaving recursive functions running forever?](https://stackoverflow.com/q/50676564/1048572) and [Why does a function with setTimeout not lead to a stack overflow](https://stackoverflow.com/q/61986701/1048572) – Bergi Jan 17 '21 at 18:53

2 Answers2

6

Let's look at each in turn.

const recSetTimeOut = () => {
  console.log('in recSetTimeOut');
  setTimeout(recSetTimeOut, 0)
};
recSetTimeOut();

This is not actually recursion. You are registering recSetTimeOut with the scheduler. When the browser's UI thread goes idle, it will pull the next waiting function off the list, an invoke it. The call stack never grows; the scheduler (native code) will always be at the top of a very short call stack. You can verify this by emitting an exception and checking its call stack.

  • This function isn't actually recursive; the stack does not grow.
  • It yields control back to the UI thread after each invocation, thereby allowing UI events to be processed.
  • The next invocation only occurs once the UI is done doing its thing and invokes the next scheduled task.
const recPromise = () => {
  console.log('in recPromise');
  Promise.resolve().then(recPromise);
}
recPromise();

This is effectively an infinite loop that refuses to ever yield control back to the UI. Every time the promise resolves, a then handler is immediately invoked. When that completes, a then handler is immediately invoked. When that completes... The UI thread will starve, and UI events will never be processed. As in the first case, the call stack does not grow, as each callback is made by what is effectively a loop. This is called "Promise chaining". If a promise resolves to a promise, that new promise is then invoked, and this does not cause the stack to grow. What it does do, though, is prevent the UI thread from doing anything.

  • Effectively an infinite loop.
  • Refuses to yield control back to the UI.
  • The call stack does not grow.
  • The next call is invoked immediately and with extreme prejudice.

You can confirm both stack traces are practically empty by using console.log((new Error()).stack).

Neither solution should result in a Stack Overflow exception, though this may be implementation-dependent; the browser's scheduler might function differently from Node's.

  • I'm not sure I follow, why does `setTimeout` not starve the UI thread since it's also executing your handler as fast as the browser can handle it? Maybe has something to do with your statement: `The next call is invoked immediately and with extreme prejudice.` but I'm not sure how... – Ruan Mendes Jul 08 '19 at 17:28
  • @JuanMendes `setTimeout` doesn't run the handler immediately. It schedules it for deferred execution, and the scheduler doesn't de-queue and execute the handler until after the UI thread is idle. –  Jul 08 '19 at 17:55
  • Promise handlers are also scheduled, that is run asynchronously, I still don't follow. I can prove it by running both the examples and you'd still see the console messages from setTimeout – Ruan Mendes Jul 08 '19 at 18:14
  • If you copy the second example into your browsers console and run it, that browser tab will stop responding to UI events. You might be able to scroll, but clicking won't do anything. This is because the UI thread is effectively tied up in an infinite loop. Promises create scheduled jobs, but in a different queue from `setTimeout` and `setInterval`, and that promise scheduler doesn't give the UI time to breathe. –  Jul 08 '19 at 18:39
  • I'd love to see a link to something that specifies this; otherwise, these are observations that are implementation dependent – Ruan Mendes Jul 08 '19 at 19:06
  • https://stackoverflow.com/questions/38059284/why-does-javascript-promise-then-handler-run-after-other-code might be what you're after. –  Jul 08 '19 at 19:31
  • Could be but [this section from MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#Timing) seems to indicate it more clearly `Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later when the queue is emptied at the end of the current run of the JavaScript event loop, i.e. pretty soon:` At the end of the current run of the JavaScript loop, which I think matches what you are saying. The stack unwinds completely, but JavaScript gets another turn instead of letting the UI do anything. – Ruan Mendes Jul 08 '19 at 19:38
-6

To my understanding of you question, this is failing because then does not accept arguments, as you are calling it.

Something like this may yield an expected result...

const recPromise = async () => {
  return Promise.resolve(recPromise())
}
camwhite
  • 837
  • 8
  • 13
  • 4
    What? `.then` does accept a *callback*, which is an *argument*. –  Jul 08 '19 at 15:16
  • then actually *receives* an arg of the resolved promise, you cannot *pass* an argument to then as you're doing. – camwhite Jul 08 '19 at 15:17
  • 2
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then : `The then() method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise.` –  Jul 08 '19 at 15:21
  • yes but what i am saying in this example you can clearly see he passes the function `recPromise` to `then`, that will not work and why it's breaking imho. – camwhite Jul 08 '19 at 15:22
  • Their usage of `.then(recPromise);` is perfectly valid. –  Jul 08 '19 at 15:23
  • why does it fail then? – camwhite Jul 08 '19 at 15:23
  • 1
    For another reason. –  Jul 08 '19 at 15:26
  • in this case you are wrong, yes if a callback was supplied by another name it would work but still resolve nothing. calling resolve with no args and immediately invoking then will result in a null value, and with this recursion will throw an error. – camwhite Jul 08 '19 at 15:29
  • *shrug* I'm not going to argue. Your answer is at -5. So apparently, 4 others agree with me. Argue your case with them, I guess. –  Jul 08 '19 at 15:30
  • 3
    Why would the callback name matter? Nothing about this answer or your reasons are correct. The value of the resolved promise *does not matter* in any way for this example. – Clint Jul 08 '19 at 15:35
  • because of the recursive nature of the function, run the code... does it throw an error? – camwhite Jul 08 '19 at 15:36
  • 1
    The code not throwing an error doesn't tell anything about whether your explanation of what's happening is correct (it is not.) – JJJ Jul 08 '19 at 18:31