3

Given the function

async function x(n) {
    console.log(n);
    console.trace();
    if (n >= 3) { return; };
    await setTimeout(() => x(n+1), 1000);
}

x(0);

I can see that the traces in console getting longer and longer.

Comparing that to the recursive version,

async function x(n) {
    console.log(n);
    console.trace();
    if (n >= 3) { return; };
    x(n+1);
}

x(0);

You can see the trace also growing. So is the setTimeout also growing the stack, or what's the explanation behind the growing trace?

Danny
  • 244
  • 3
  • 14
  • The stack keeps trace of what called the function, even when there is a setTimeout, can you be more precise about what you expected? As it is, we cannot really answer anything else that "that's the expected behavior" – Kaddath Jun 21 '23 at 07:41
  • Both functions are recursive, i can't see any difference. – kevinSpaceyIsKeyserSöze Jun 21 '23 at 09:14
  • 2
    @kevinSpaceyIsKeyserSöze The first one is not really recursive. [Some](https://stackoverflow.com/q/68375903/1048572) [people](https://stackoverflow.com/a/29144411/1048572) call it [pseudo-recursion](https://stackoverflow.com/q/40390979/1048572) though – Bergi Jun 21 '23 at 09:38
  • Does this answer your question? [Why does setTimeout() clutter my call stack under Chrome DevTools?](https://stackoverflow.com/questions/47585441/why-does-settimeout-clutter-my-call-stack-under-chrome-devtools) – Parzh from Ukraine Jun 21 '23 at 10:36

2 Answers2

2

No, the stack isn't growing in the setTimeout version. What you're seeing in the output of console.trace() is described thusly in the MDN docs:

Note: In some browsers, console.trace() may also output the sequence of calls and asynchronous events leading to the current console.trace() which are not on the call stack — to help identify the origin of the current event evaluation loop.

You can see this at least in Chromium-based browsers, if instead of using console.trace(), you instead get hold of the current actual stack:

async function x(n) {
  console.log(n);
  console.log(new Error().stack);
  if (n >= 3) {
    return;
  };
  await setTimeout(() => x(n + 1), 1000);
}

x(0);

The results of this may differ in other browsers as .stack is a non-standard property, but at least in Chromium it shows the current actual stack triggered from the JavaScript event loop.

Fraction
  • 11,668
  • 5
  • 28
  • 48
James Thorpe
  • 31,411
  • 5
  • 72
  • 93
  • With your code the stack log also increases in Firefox, it just displays in a different format – Kaddath Jun 21 '23 at 07:50
  • @Kaddath Interesting - it's also a non-standard property, so browsers are free to do what they like with it. In Chrome it shows just the current call stack. – James Thorpe Jun 21 '23 at 07:53
  • Maybe that's why the OP's question seemed obvious to me, didn't fall to Chrome's massive advertisement campaign and stuck with the fox, never had problems with it – Kaddath Jun 21 '23 at 07:56
  • @Kaddath Really depends what the underlying question is I guess. One view of the call stack to know how a particular piece of code is triggered, vs the actual technical call stack that's actually going on in the engine - my answer is talking about the latter. – James Thorpe Jun 21 '23 at 07:58
  • 1
    why are you awaiting setTimeout? – kevinSpaceyIsKeyserSöze Jun 21 '23 at 09:12
  • 1
    @kevinSpaceyIsKeyserSöze because I copy/pasted the OP code :) – James Thorpe Jun 21 '23 at 09:30
1

In both the setTimeout and recursive versions of the function, the trace in the console is growing because each call to the function is adding a new entry to the call stack. In the case of the recursive version, the call stack grows until the maximum call stack size is reached and the program crashes with a "Maximum call stack size exceeded" error.

In the case of the setTimeout version, the call stack is not growing indefinitely because the setTimeout function schedules the next call to the function to be executed after a delay of 1000 milliseconds. This means that each call to the function is completed and removed from the call stack before the next call is executed. However, each call to the function still adds a new entry to the call stack, which is why the trace in the console is growing.

To check that the call stack is not increasing, you can use the console.time() and console.timeEnd() methods to measure the time it takes for each call to the function to complete. If the time between each call is consistent, it means that the call stack is not growing. Here's an example:

async function x(n) {
  console.log(n);
  console.trace();
  if (n >= 3) {
    return;
  };
  const startTime = Date.now();
  await setTimeout(() => {
    console.timeEnd(`x(${n})`);
    x(n + 1);
  }, 1000);
  console.time(`x(${n + 1})`);
}
 console.time(`x(0)`);
x(0);
Fraction
  • 11,668
  • 5
  • 28
  • 48
  • 1
    "*If the time between each call is consistent, it means that the call stack is not growing.*" - not really, no. – Bergi Jun 21 '23 at 08:44
  • Thank you for your comment, @Bergi. I appreciate you sharing your perspective. Could you please explain further why my statement is not accurate? I am always looking to learn and improve my understanding – Leonzio Rossi Jun 21 '23 at 14:09
  • Do you mean that even if the time between each call is consistent, the call stack may still be growing, depending on the specifics of the code. – Leonzio Rossi Jun 21 '23 at 14:11
  • 1
    Yes. A function call is growing the stack by a fixed amount, it's a constant-time operation. If you repeat this forever (infinite recursion), the call stack is still growing, though each call takes the same time. Secondly, your `console.time` is measuring the `setTimeout`, which is far too inaccurate to say anything about the nanoseconds it takes to allocate a stack frame. – Bergi Jun 21 '23 at 16:02