Mehran
Mehran

Reputation: 16861

Why recursive async functions in Javascript lead to stack overflow?

Consider this snippet:

function f() {
  return new Promise((resolve, reject) => {
    f().then(() => {
      resolve();
    });
  });
}

f();

which can also be written like this:

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

f();

If you run any of the given two codes, you'll face this error:

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

My question is why? Before answering my question, please consider my argument:

I understand the concept of recursion and how it leads to a stack overflow if there's no stopping condition. But my argument here is that once the first f(); is executed, it will return a Promise and it exits the stack so this recursion should not face any stack overflow ever. To me, this should behave the same as:

while (1) {}

Of course, if I write it like this, it will be fixed:

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

f();

which is a different story and I have no problem with it.

[UPDATE]

My bad, I forgot to mention I was testing with node v8.10.0 on the server side.

Upvotes: 6

Views: 455

Answers (2)

Mehran
Mehran

Reputation: 16861

Thanks to @Adrian, I managed to find a way not to face the stack overflow. But before that, he was right and that form a recursion should have led to stack overflow. And since the question is "why", his answer is the accepted one. This is my attempt on "how" not to face the stack overflow.

Test 1

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

And using await:

Test 2

async function f() {
  return await Promise.resolve()
    .then(f);
}

I'm not sure if the Promise can be eliminated in this case!

And I know I said no setTimeout but this is an interesting case too:

Test 3

async function f() {
  await new Promise(resolve => setTimeout(resolve, 0));
  return f();
}

This will not face the stack overflow as well.

In the end, to give you a context why I was interested in this; let's say you are coding a function to retrieve all the records from AWS' DynamoDb. Since there's a limitation on how many records you can extract from DynamoDb with one request, you have to send as many as needed (with ExclusiveStartKey) to get all the records:

Test 4

async function getAllRecords(records = [], ExclusiveStartKey = undefined) {
    let params = {
        TableName: 'SomeTable',
        ExclusiveStartKey,
    };

    const data = await docClient.scan(params).promise();
    if (typeof data.LastEvaluatedKey !== "undefined") {
        return getAllRecords(records.concat(data.Items), data.LastEvaluatedKey);
    }
    else {
        return records.concat(data.Items);
    }
}

I wanted to make sure this will not face a stack overflow EVER. It was not feasible to have such a huge DynamoDb table to actually test this. So I came up with some examples to make sure of that.

At first, it seemed like that test #4 could actually face a stack overflow, but my test #3 shows that there's no such possibility (because of the await docClient.scan(params).promise()).

[UPDATE]

Thanks to @Bergi, here's a code for await without a Promise:

Test 5

async function f() {
  await undefined;
  return f();
}

Upvotes: 1

Adrian Brand
Adrian Brand

Reputation: 21638

Why would you not expect it to cause infinite recursion? The constructor of the promise is calling f recursively so the promise never gets to construct as an infinite recursion loop occurs before the promise is constructed.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

From the link above

The executor function is executed immediately by the Promise implementation, passing resolve and reject functions (the executor is called before the Promise constructor even returns the created object).

Upvotes: 6

Related Questions