2

for await of — supposed to be used with asynchronous iterators, it awaits each promise within iterator and handles response of each awaited promise in its body. From MDN docs this syntax can be used also with sync iterators, so

I have the following code examples that work similar:

(async () => {
    const entityIds = [1, 2, 3, 4];
    
    for await (const id of entityIds) {
        console.log(await getEntityById(id));
    }
})();

And:

(async () => {
    const entityIds = [1, 2, 3, 4];
    
    for (const id of entityIds) {
        console.log(await getEntityById(id)); // ESLint: Unexpected `await` inside a loop.(no-await-in-loop)
    }
})();

But in the 2nd example I'm getting eslint warning about the rule no-await-in-loop. The question is: why? Each iteration of these 2 loops will await for async function completes, but the eslint is not gonna react on 1st case at all.

Denys Rybkin
  • 637
  • 1
  • 6
  • 18
  • In the first case, ESLint cannot know whether you're using synchronous or asynchronous iterable. It doesn't do that kind of control flow analysis. It just knows you've used `for await...of` which means your code is already not parallel. No reason to warn you about potentially making your code parallel. – VLAZ Feb 13 '22 at 11:12
  • 1
    @VLAZ - Using an async iterable doesn't necessarily mean your code can't be parallel (`await Promise.all(theAsyncIterable)`). The rule is just dumb. :-) It assumes everything should be done in parallel, despite that being fairly obviously not always true. @​Denis - I'd just turn it off. – T.J. Crowder Feb 13 '22 at 11:14
  • @T.J.Crowder I know, however, that's what the rule does - prevents you from using `await` in order to parallel. And `for await...of` cannot be guaranteed to work the same when running in parallel. Example - next result relies on the previous result being computed. Hence why the rule doesn't trigger. And for the record, I agree it's not terribly useful. There are many cases where you'd want sequential processing. – VLAZ Feb 13 '22 at 11:16
  • @VLAZ - It's not just `for-await-of`, `for` (with an `await`) also can't be guaranteed to work the same in parallel. Applying the rule to the latter and not to the former doesn't make any sense to me. – T.J. Crowder Feb 13 '22 at 11:19
  • @T.J.Crowder I think we can both agree it's not a very useful rule. We should also be able to agree that given why it exists, it's not even a very well designed rule. I'm not trying to defend it. I'm explaining how it works. Looking [at the test cases](https://github.com/eslint/eslint/blob/781f8d0e2400f4a3c4b633b43d318ef91767c6c4/tests/lib/rules/no-await-in-loop.js) `for await of` is considered "intentional" and thus the rule would not trigger. I guess the the reasoning is: consuming an async iterable in parallel is *more likely* to be wrong than consuming a sync one in parallel. – VLAZ Feb 13 '22 at 11:28
  • @T.J.Crowder So, can I just use `for await` in order not to disable the eslint rule everytime? (yes, I realise I can disable this rule in my eslint config, but) – Denys Rybkin Feb 13 '22 at 11:28
  • 1
    @DenisRybkin - **I** wouldn't, it introduces an async "tick" into the processing of the loop, but you could if you like. Maybe that doesn't matter, but maybe it does. Again, I'd just disable it entirely. :-) – T.J. Crowder Feb 13 '22 at 11:30
  • @VLAZ - My mistake, I thought you were supporting the rule author's choice not to apply the rule consistently. :-) – T.J. Crowder Feb 13 '22 at 11:34
  • @T.J.Crowder "I wouldn't, it introduces an async "tick" into the processing of the loop" -- do you mean it's like using "const res = async () => await smthAsync()" ? With useless await. But in case `for await of`, await applies to an array that is iterable? – Denys Rybkin Feb 13 '22 at 11:36
  • 2
    @DenisRybkin if you consume a synchronous iterable which produces synchronous values using `for await of` you'd (effectively) get each one wrapped in an extra promise to be awaited. Which is useless. Option 1 is to disable the rule for this case with a comment and use reguler `for...of` or option 2 is what T.J. Crowder suggests and remove the rule entirely. I also support option 2. You seem you know enough about `await` to know when you want it sequential and when not. – VLAZ Feb 13 '22 at 11:39
  • 2
    @DenisRybkin - `y = await x` where `x` isn't a thenable is like `Promise.resolve(x).then(x => y = x)`. It adds a microtask to the current task's microtask queue, then returns, then gets picked up when it's reached in the microtask processing at the end of the task. You'd be doing it on every loop iteration. That's a lot of async run-arounds if the process would be synchronous otherwise. :-) – T.J. Crowder Feb 13 '22 at 11:42
  • @T.J.Crowder O-o-okay, maybe it requires another post, but, please, tell me then, why `for await of` was designed to consume sync iterable also? Is there any meaningful use cases for that? @VLAZ – Denys Rybkin Feb 13 '22 at 11:49
  • And yeah, please, post your both answers I'll vote them up – Denys Rybkin Feb 13 '22 at 11:51
  • 2
    @DenisRybkin the intention of `for await of` consuming synchronous iterables is if they actually produce promises for `.next().value`. While async iterables produce a promise from `.next()` itself. Then both types of iterations can be unified under the same construct since `for await (const x of xs)` is always giving you plain value, whteher `xs` is async iterable or a sync one that feeds you promises. For consistency reasons, consuming a sync iterable which doesn't produce promises is still possible. Just not great. – VLAZ Feb 13 '22 at 11:54
  • 1
    @DenisRybkin - It's for the same reason `await` can consume a non-thenable. Since JavaScript doesn't have compile-time type information, the JavaScript engine can't know until actually executing the code whether the value `await` is being used on is a thenable or not. So the designers of `await` had a choice: Either make it throw an error on a non-thenable value, or just wrap it in a promise by using `Promise.resolve(theValue)` and `await` the resulting promise. They went with option 2 (which I think was probably for the best). – T.J. Crowder Feb 13 '22 at 11:55
  • @T.J.Crowder "*using Promise.resolve(theValue) and awaiting the resulting promise. They went with option 2 (which I think was probably for the best).*" related to that, [here is a useful article](https://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/) on async APIs and *why* you want them to stay async even if they could be mixed (e.g., `await 5` could finish without a tick but it shouldn't). – VLAZ Feb 13 '22 at 12:00
  • 1
    "*From MDN docs this syntax can be used also with sync iterators*" - [it absolutely should not, though](https://stackoverflow.com/a/59695815/1048572). – Bergi Feb 13 '22 at 12:45

1 Answers1

4

The no-await-in-loop rule in ESLint is supposed to protect you from accidentally processing some data sequentially. The reasoning is that often having an await in a loop is not intentional because it is more efficient to process the data in parallel. Thus all usages of await in a loop are considered a "mistake" when the rule is turned on.

Using for await...of is explicitly considered "intentional" by the rule. Checking the rule's tests (bottom of the documentation page) the following part is listed as valid code:

valid: [
    // ...

    // Asynchronous iteration intentionally
    "async function foo() { for await (var x of xs) { await f(x) } }"
],

while any usage of await in any other loop construct is invalid. Including using await inside a loop inside a for await...of:

invalid: [
    // ...

    // In a nested loop of for-await-of
    { code: "async function foo() { for await (var x of xs) { while (1) await f(x) } }", errors: [error] }
]

The reasoning most likely is that using for await...of you opt into sequential processing. If you process an asynchronous iterable, chances are very high that you cannot do that in parallel and it would be an error if you try. If you are processing a synchronous iterable which produces promises there is still a chance that you cannot correctly do this in parallel.

ESLint cannot actually detect if you instead have a synchronous iterable which produces synchronous results like an array iterator, therefore, it assumes it is one of the first two options.


In my opinion, the rule is flawed as it is all or nothing. Enabling it means you want no loops with await in them. There are still valid cases where you want sequential processing with for...of and an await inside. Which means that your code is likely to be littered with temporary disables of the rule whenever you do not need it.

My suggestion is to disable the no-await-in-loop rule and exercise common sense for when you use sequential and when you use parallel processing.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • 2
    "*If you process an asynchronous iterable, chances are very high that you cannot do that in parallel*" - there isn't even any syntax to do that. Asynchronous iteration is always sequential. – Bergi Feb 13 '22 at 12:48
  • @Bergi The statement is IMO correct. Only the obtaining of iterable properties is sequential. The loop body may still invoke a long running processing which may run in parallel (each step just started a bit later). – Jan Molnár Aug 17 '22 at 08:26