How Does JavaScript Handle Asynchronous Recursion in Call Stacks for Deeply Nested Promises?

I’m working on an application where I need to recursively process a deeply nested structure using asynchronous operations (Promised-based API calls), and I’m curious about how JavaScript’s event loop, call stack, and microtask queue manage this. Specifically, I want to understand how recursion interacts with the asynchronous flow of JavaScript.

Here’s a simplified example of the recursion pattern I’m using:

async function processNode(node) {
    if (node.children) {
        for (let child of node.children) {
            await processNode(child);
        }
    }
    return performAsyncTask(node); // Assume this returns a Promise
}

My questions are:

  1. How does JavaScript manage the call stack when processNode is deeply recursive? Does the stack get cleared between async calls due to the await, or can it still lead to stack overflow with deep recursion?

  2. When an await is encountered, I know the function execution is paused and the event loop takes over. At what point does the function get re-entered into the call stack? Does the stack grow with every await resolution in the recursive call?

  3. What strategies could be used to prevent potential stack overflow with deeply recursive async functions?

I’ve seen some discussions around tail call optimization in JavaScript, but I’m unsure how it applies in the case of async/await.

I’d appreciate some clarity on the internal mechanics of the call stack and event loop when recursion and async operations are combined!