Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't an infinitely recursive async function cause stack overflow?

I was thinking what happens when an async function recursively calls itself infinitely. My thought was that it will not cause stack overflow. But I can't exactly point out why this is the case.

const foo = async () => {
    const txt = await Promise.resolve("foo");
    console.log(txt);
    foo();
}

foo();

The code above prints "foo" infinitely without overflowing the stack.

My idea is that the code is conceptually similar to the following, it doesn't cause stack overflow because the recursive call to foo() is inside the callback, the original call to foo() will return before that.

const bar = () => {
    console.log("foo");
    foo();
}

const foo = () => {
    setImmediate(bar);
}

foo();

I am looking for the exact answer as to what happens in the case of the async function.

like image 657
Raju Ahmed Avatar asked May 19 '19 06:05

Raju Ahmed


People also ask

Does infinite recursion cause stack overflow?

The most-common cause of stack overflow is excessively deep or infinite recursion, in which a function calls itself so many times that the space needed to store the variables and information associated with each call is more than can fit on the stack.

How does recursive function prevent stack overflow?

A general method for avoiding a stack overflow is to include what's called a "bootstrap condition" within the recursion. It's some condition that gets hit every time the function calls itself. You set the condition to something that causes the function to return when some state is reached, thereby unwinding the stack.

Can a recursive function run forever?

If a recursion never reaches a base case, it will go on making recursive calls forever and the program will never terminate. This is known as infinite recursion, and it is generally not considered a good idea. In most programming environments, a program with an infinite recursion will not really run forever.

How does a recursive function avoid an infinite loop?

There should always be two parts to a recursive function: the recursive case and the base case. The recursive case is when the function calls itself. The base case is when the function stops calling itself. This prevents infinite loops.


2 Answers

This function is syntactic sugar for

const foo = () => 
  Promise.resolve(
    Promise.resolve("foo")
    .then(txt => {
      console.log(txt);
      foo();
    })
  );

foo();

This itself can be rewritten with less dependencies as

const foo = () =>
  queueMicrotask(() =>
    queueMicrotask(() => {
      console.log("foo");
      foo();
    })
  );
foo();

Window.queueMicrotask is a quite new method that gives us a way to trigger the queue a microtask operation that Promise.resolve and hence await do trigger.
Basically, this operation pushes the microtask at the end of the current execution, but before the end of the current event loop.

The sixth point of the algorithm reads

Set task's script evaluation environment settings object set to an empty set.

This is why you don't have a stack-overflow here. However, since you are never exiting the event loop, you are blocking the browser.

like image 51
Kaiido Avatar answered Oct 22 '22 22:10

Kaiido


Your code doesn't produce stack overflow because when you have call foo inside function it is not awaited. If you write await foo(); then it should cause stack overflow.

Consider below two cases:

Case 1 Here as per your code. From a() it will call foo without await. So what will happen when it calls foo() as it is async function, it would be scheduled to run after the current execution resolves. Or even more precisely, it will be queued for later execution and immediately a() will also continue from next line. You can see the output that a() is getting ended first, it doesn't wait for call stack for foo to return;

const foo = async () => {
    const txt = await Promise.resolve("foo");
    console.log(txt);
}

const a = async () => {
    const txt = await Promise.resolve("a");
    console.log(txt);
    foo();
    console.log("-- ENd of a() --");
}

a();

Case 2 Here inside a() it will call foo with await. You can see the output that a() is waiting for return from foo() then only it will continue on next line.

const foo = async () => {
    const txt = await Promise.resolve("foo");
    console.log(txt);
}

const a = async () => {
    const txt = await Promise.resolve("a");
    console.log(txt);
    await foo();
    console.log("-- ENd of a() --");
}

a();
like image 2
Karan Avatar answered Oct 22 '22 22:10

Karan