Why the Function Called by Settimeout Has No Callstack Limit

Why the function called by setTimeout has no callstack limit?

setTimeout is asynchronous (it returns before executing the callback), and the callback will be executed on a new, empty stack frame. That's the whole purpose. It will never overflow the stack.

It is not a recursive call, for which scope (in case of a non-tail-call-optimized function) would need to be retained. But that would also mean that the function became blocking, which is not what you want.

why does a function with setTimeout not lead to a stack overflow

Because it is no longer recursive. At least not technically.

The source code does look recursive so a programmer may write such code as if it is recursive but from the point of view of the CPU it is no longer recursive. It is processed sequentially in a loop.

Recursion and the stack

A recursive function calls itself. What happens when this happens is that the stack keeps increasing until the last function returns. A function's stack frame isn't removed from the stack until the function returns (let's ignore closures for now) so because recursive function calls itself it won't return until that call to itself returns. This is what causes the stack to grow.

Tail recursion

Languages such as Lisp, Haskell and Scala recognize that there are some cases where a stack frame can be released while doing recursion. Generally, if the recursive call is the last instruction in the function and no other processing is done to the return value you can remove the current stack frame because it will no longer be used after the recursive function returns. Therefore, such languages implement what's called tail recursion: the ability to recurse infinitely without growing the stack.

This is especially useful for very pure functional languages where the only programming structure you have is functions because without the existence of statements you cannot have loop statements or conditional statements etc. Tail recursion makes infinite loops in Lisp possible.

However, Javascript does not have tail recursion. So this does not affect how recursion behaves in Javascript. I mention this to note that not all recursion need to grow the stack.

Scheduling

Timer functions such as setTimeout() and setInterval() does not call the functions passed to them. Not only do they not call them immediately, they don't call them at all. All they do is pass the function to the event loop along with information of when should the function be called.

The event loop is essentially the core of javascript. The interpreter enters the event loop if and only if there is no more javascript to execute. You can think of the event loop as the idle state of the interpreter. The event loop continuously checks for events (I/O, UI, timer etc.) and execute the relevant functions attached to the event. This is the function you passed to setTimeout().

setTimeout

So with the facts given above we can see how "recursion" via setTimeout is not really recursion.

  1. First your function calls setTimeout and passes itself to it.

  2. setTimeout saves the function reference to a list of event listeners and sets up the timer to trigger the event which will trigger the function

  3. Your function continues and returns, note that the "recursed" function is not yet called. Since your function returns it's stack frame gets removed from the stack.

  4. Javascript enters the event loop (there's no more javascript to process).

  5. Timer for your function expires and the event loop calls it. Repeat until you stop calling setTimeout

Does the set timeout web API have no memory limit?

To answer your question which is :

Does the set timeout web API have no memory limit?

I would say from what I found searching around to help you is that you may run into the problem of memory limitations available to node with setTimeOut function.

Read more about it here

Is there will be any stack overflow error with setTimeout() in 1st function and closure 2nd function in javascript?

No, there won't be.

Timers "set aside" the callback - they don't call them in the same execution stack as the one that's currently running (the one calling setTimeout). JS will continue executing the current script until the stack is empty before calling any callback set by a timer (or any completed async operation for that matter).

In your case precalculationsInit would "schedule" precalculationsInitClosure, then finish executing, emptying the stack. Then at least 10ms later, JS would call the scheduled precalculationsInitClosure, do stuff, then call precalculationsInit and repeats the same procedure.

Your stack would look like:

(empty)
pI (schedules pIC)
(empty, JS looks for callbacks)
pIC
pIC -> pI (schedules pIC)
pIC
(empty, JS looks for callbacks)
pIC
pIC -> pI (schedules pIC)
pIC
(empty)
...and so on

As you can see, because of this "scheduling"/"setting aside", the stack does not build up like in recursion:

pI
pI -> pIC
pI -> pIC -> pI
pI -> pIC -> pI -> pIC
...and so on

Maximum call stack size exceeded error

It means that somewhere in your code, you are calling a function which in turn calls another function and so forth, until you hit the call stack limit.

This is almost always because of a recursive function with a base case that isn't being met.

Viewing the stack

Consider this code...

(function a() {
a();
})();

Here is the stack after a handful of calls...

Web Inspector

As you can see, the call stack grows until it hits a limit: the browser hardcoded stack size or memory exhaustion.

In order to fix it, ensure that your recursive function has a base case which is able to be met...

(function a(x) {
// The following condition
// is the base case.
if ( ! x) {
return;
}
a(--x);
})(10);

By using javascript recursive setTimeout function, is it risky to get the stackoverflow?

By using javascript recursive setTimeout function, is it risky to get the stackoverflow?

No. setTimeout registers a handler that will get called by the browser when the timer triggers. By the time that happens, the stack has unwound (if it hadn't, the task that scheduled the timeout wouldn't have ended, and the browser's UI would be locked up waiting for it to end).

By trying this example you can see in browser console that the stack grows.

No, the stack unwinds before the handler is next called. If you're referring to the async "stack" entries that Chrome's devtools show you, those aren't the real stack, and they're a devtools artifact. (Start your timer, watch for two ticks so you see the "async" entry on the second one, then close your console, wait two more ticks, and reopen it; notice that there aren't any "async" entries — at all, not even the one you saw it log before closing the console!)

Why such recursion not getting stack-overflowed?

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.



Related Topics



Leave a reply



Submit