What causes stack overflow in recursive async functions in JavaScript?

I have a couple of code snippets that illustrate a problem with recursive async functions. For instance:

function example() {
  return new Promise((resolve) => {
    example().then(() => {
      resolve();
    });
  });
}

example();

This can also be expressed as:

async function example() {
  return await example();
}

example();

Running either of these snippets results in an error:

(node:23197) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded

I would like to understand why this is happening. I grasp the recursion concept and how it can lead to a stack overflow in the absence of a base case. However, I believe that after the first call to example();, it merely returns a Promise and exits the stack, so I don’t see how this could lead to a stack overflow. It seems to me that this should behave similarly to:

while (true) {}

Here’s an alternative that avoids the problem:

function example() {
  return new Promise((resolve) => {
    setTimeout(() => {
      example().then(() => {
        resolve();
      });
    }, 0);
  });
}

example();

This one works fine without any issues. Just to clarify, I am testing this on node v8.10.0.

hey! the recursive async calls without delay like setTimeout are quick n stack overflow can happen quickly because js uses call stack n creating infinite promises without a break floods the stack fast. Recursive calls need a base case or delays, else it’s stack chaos! :slightly_smiling_face:

An aspect to consider is how JavaScript handles promise resolution. Each recursive call returns a promise that needs to be fulfilled, and the call stack retains information about each of these calls until they resolve. This retention uses up stack space as promises continue to pile up without being handled immediately. When a recursive function doesn’t have a base case or incorporates an asynchronous delay, the stack becomes overwhelmed. Using setTimeout effectively queues the function for the next execution cycle, giving the stack a chance to reset after each execution. Hence, this strategizes around this limitation, enabling smoother recursion.

The problem arises because JavaScript’s call stack has a size limit, and each recursive call without a delay or a base case adds to this stack. With recursive async functions, removing the stack frames through returning promises isn’t instantaneous, leading to accumulation until the stack overflows. When you use setTimeout, it allows the call stack to unwind before making the next recursive call by deferring execution. This approach prevents the stack from becoming saturated and breaking continuity, which is why it operates without errors.

Recursive async can be tricky! Every recursive call on a new promise keeps adding to the stack until it blows up. Just like in recursion, without a base, it goes on & hits the limit. setTimeout gives breathing space by delaying calls, easing stack pressure. try it out!