2

I have a function that writes data to a mongodb, like so:

const writeToDB = async (db, data) => {
  const dataKeys = Object.keys(data)
  dataKeys.forEach(async key => db.collection(key).insertMany(data[key]))
}

This works fine if I run it in a node script. But when I tried to use it in Jest's beforeAll I got this async error from Jest:

Jest did not exit one second after the test run has completed. This usually means that there are asynchronous operations that weren't stopped in your tests.

after some troubleshooting I found out that forEach was causing the trouble. Using a for loop solved this problem:

const writeToDB = async (db, data) => {
  const dataKeys = Object.keys(data)
  for (const key of dataKeys) {
    await db.collection(key).insertMany(data[key])
  }
}

Searching for this problem I came across this article: https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404

The explanation there made sense, but it left me with some questions:

  • According to the article, it should not have worked even in the node script. How come it did?
  • Why is executing it in node different from running it in Jest?

edit

After reading all the comments, I realise my first question was a bit nonsense. Normally I assign the result of an async function to a variable, and if I don't put await, there will an undefined error down the line. But that's not the case here, so script exits normally and the db writes happen concurrently in the background.

devboell
  • 1,180
  • 2
  • 16
  • 34
  • `forEach` doesn't `await` for an `async` callback to settle. – nicholaswmin Feb 18 '19 at 16:07
  • 1
    Jest complains because the process is still running after the test ends - the process cannot finish until the promises have all been settled. Node doesn't mind because there's no such check, it just keeps the process running. You could also write `await Promise.all(dataKeys.map((key) => db.collection(key).insertMany(data[key]));`, which allows for parallel operations. – jonrsharpe Feb 18 '19 at 16:08
  • @jonrsharpe, but why did the process finish properly when I ran it in a normal node script? – devboell Feb 18 '19 at 16:10
  • 1
    Why wouldn't it? The process keeps going - that's what Jest is complaining about, if you read the error message, that there's still something happening in the background even though the tests have all finished. – jonrsharpe Feb 18 '19 at 16:11
  • Why is the callback to forEach **async** if you're not **await**ing it? You're missing something here. – Azami Feb 18 '19 at 16:12
  • @MadWard, As I understood it from the no-return-await eslint rule, an await is not required in this case – devboell Feb 18 '19 at 16:20
  • 1
    What exactly do you mean by "*This works fine if I run it in a node script.*". How does it work "fine", what did you test for? Are you surprised that it did run all the insertions? – Bergi Feb 18 '19 at 16:35
  • 1
    *According to the article, it should not have worked even in the node script* - depends on the script. *Why is executing it in node different from running it in Jest?* - there's no evidence that it was executed differently. There's no warning because this is Jest's warning. – Estus Flask Feb 18 '19 at 16:47
  • Possible duplicate of https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop – Kevin B Feb 18 '19 at 16:55

2 Answers2

7

The existing answer already explains in detail why forEach shouldn't be used with promises the way it's used. forEach callback doesn't take returned promises into account and breaks promise chain. async..await needs to be used with for..of to evaluate promises in series or with Promise.all and map to evaluate in parallel.

Jest supports promises and expects that a promise that is returned from asynchronous function (it, etc) signifies that asynchronous process that occurred in this function has ended.

Once Jest finishes a test, it checks if there are open handles that prevent Node from exiting. Since promises weren't returned and chained by Jest, processes that they represent prevent Jest from finishing test process.

This problem is represented by said error message:

Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
3

Async functions work even in contexts that don't await or call .then() on them, that is, I can definitely do this:

async function foo() {
  // async calls here
}

foo(); // no await or .then().

This means you cannot wait for the operation to finish, you can't use the value, and worst of all, you cannot catch or recover from any async errors that might be thrown (or rejected, if we're being accurate)

The main difference is that the .forEach() doesn't care or wait for the operations to finish before calling the next one (since async functions return immediately), whereas your for..of call uses await to wait for each operation to complete before moving on to the next.

Your first .forEach example would be roughly equivalent to the bottom one if you removed the await from the call inside of the loop.

The result is that your first example returns a Promise that resolves immediately, rather than after all of your DB calls were finished, so the test expects the operations to finish, but they are not. In the second example, you properly await for all the calls to finish before the async function finishes, so the return Promise will only resolve after all of the calls resolve themselves, first.


On that note, the two examples are not equivalent because the first would call the insertMany one after the other without waiting for them to finish, causing the DB calls to perform in parallel.

If you want to preserve this behavior, but still return a correct Promise which waits for everything to finish, you should be using [].map() instead of [].forEach:

const writeToDB = async (db, data) => {
  const dataKeys = Object.keys(data)
  const allPromises = dataKeys.map(async key =>
    await db.collection(key).insertMany(data[key])) // run all calls in parallel
  return await Promise.all(allPromises); // wait for them all to finish
}
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
  • I think you answered my first question. It's not that node would throw an error if forEach doesn't await, it just doesn't care and executes in parallel, as you mention. But why does jest care? how does it detect that promises were not awaited? – devboell Feb 18 '19 at 16:26
  • Thanks, I accepted estus answer, because he addressed the second question. But it's my mistake, I shouldn't have asked two questions at once. – devboell Feb 18 '19 at 17:54
  • @devboell Well, Jest is a test runner, it's meant to find those things because they are indicative of bugs :) – Madara's Ghost Feb 18 '19 at 22:10