no-await-in-loop
Disallow await inside loops without considering concurrency implications
Disallow await inside loops without considering concurrency implications. This rule is part of eslint-plugin-reliability and provides LLM-optimized error messages with concurrency pattern suggestions.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Warning (performance) |
| Auto-Fix | β No (suggests refactoring patterns) |
| Category | Quality |
| ESLint MCP | β Optimized for ESLint MCP integration |
| Best For | Performance-critical applications, API batch operations |
Rule Details
Why This Matters
| Issue | Impact | Solution |
|---|---|---|
| β‘ Performance | N*latency instead of 1x | Use Promise.all() |
| π Sequential Bottleneck | Blocks event loop | Concurrent execution |
| π Scalability | Slow with large datasets | Batch processing |
| π― Resource Usage | Inefficient API calls | Parallelization |
What is reported
An await that runs on every iteration: in the loop body, in a nested block of
it, or in a per-iteration header slot (a for test or update, a while /
do-while condition). Not reported:
- an
awaitin a slot evaluated once before iterating (theforβ¦of/forβ¦initerable, aforinitialiser); - an
awaitinside a function defined in the loop β it runs in its own async context; - a loop that carries an awaited value into its next iteration: a cursor the
loop test reads, an accumulator passed to the next
await, a status that decides abreakorreturn. Those iterations cannot run concurrently, so the sequential loop is the correct shape.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
allowForOf | boolean | false | Allow await in for-of loops |
allowWhile | boolean | false | Allow await in while and do-while loops |
checkConcurrency | boolean | true | Shapes the advice in the message; never whether a loop is reported |
Examples
β Incorrect
// Sequential execution - N * latency
async function fetchAllUsers(ids: string[]) {
const users = [];
for (const id of ids) {
const user = await fetchUser(id); // β Await in loop
users.push(user);
}
return users;
}
// Also problematic
async function processItems(items: Item[]) {
for (let i = 0; i < items.length; i++) {
await processItem(items[i]); // β Await in loop
}
}β Correct
// Concurrent execution - 1x latency (max)
async function fetchAllUsers(ids: string[]) {
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users;
}
// With error handling
async function processItems(items: Item[]) {
const results = await Promise.allSettled(
items.map(item => processItem(item))
);
return results;
}
// Controlled concurrency with p-map
import pMap from 'p-map';
async function fetchWithLimit(urls: string[]) {
return pMap(urls, fetchUrl, { concurrency: 5 });
}Configuration Examples
Basic Usage
{
rules: {
'reliability/no-await-in-loop': 'warn'
}
}Allow for-of Loops
{
rules: {
'reliability/no-await-in-loop': ['warn', {
allowForOf: true,
allowWhile: false
}]
}
}Strict Mode
{
rules: {
'reliability/no-await-in-loop': ['error', {
allowForOf: false,
allowWhile: false,
checkConcurrency: true
}]
}
}Refactoring Patterns
Sequential to Parallel
// β Before: 10 items = 10 * 100ms = 1000ms
for (const item of items) {
await process(item);
}
// β
After: 10 items = ~100ms (parallel)
await Promise.all(items.map(process));With Rate Limiting
// Using p-map for controlled concurrency
import pMap from 'p-map';
async function fetchAll(urls: string[]) {
return pMap(urls, fetch, { concurrency: 3 });
}When Sequential is Required
The rule stays silent when an iteration needs the previous one's awaited result:
// β
Not reported: the cursor the test reads comes from the await
let cursor = start;
while (cursor) {
const page = await getPage(cursor);
cursor = page.next;
}
// β
Not reported: the accumulator feeds the next await
async function processSequentially(items: Item[]) {
const results = [];
for (const item of items) {
// Dependencies between iterations require sequential
const result = await processWithPrevious(item, results);
results.push(result);
}
return results;
}When Not To Use
| Scenario | Recommendation |
|---|---|
| π Order-dependent ops | Use allowForOf: true or disable rule |
| π Rate-limited APIs | Use controlled concurrency libraries |
| πΎ Memory constraints | Sequential may be necessary |
| π Transaction chains | Sequential execution required |
Comparison with Alternatives
| Feature | no-await-in-loop | eslint built-in | unicorn |
|---|---|---|---|
| Concurrency Check | β Yes | β No | β οΈ Limited |
| Loop Type Config | β Per-type | β No | β No |
| LLM-Optimized | β Yes | β No | β No |
| ESLint MCP | β Optimized | β No | β No |
| Pattern Suggestions | β Yes | β No | β οΈ Limited |
Related Rules
detect-n-plus-one-queries- Detects N+1 query patternsno-blocking-operations- Detects blocking operations
Further Reading
- Promise.all() - MDN - Concurrent promise execution
- Promise.allSettled() - MDN - Handle mixed results
- p-map - Controlled concurrency library
- ESLint MCP Setup - Enable AI assistant integration
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Dynamic Variable References
Why: Static analysis cannot trace values stored in variables or passed through function parameters.
// β NOT DETECTED - Value from variable
const value = externalSource();
processValue(value); // Variable origin not trackedMitigation: Implement runtime validation and review code manually. Consider using TypeScript branded types for validated inputs.
Wrapped or Aliased Functions
Why: Custom wrapper functions or aliased methods are not recognized by the rule.
// β NOT DETECTED - Custom wrapper
function myWrapper(data) {
return internalApi(data); // Wrapper not analyzed
}
myWrapper(unsafeInput);Mitigation: Apply this rule's principles to wrapper function implementations. Avoid aliasing security-sensitive functions.
Imported Values
Why: When values come from imports, the rule cannot analyze their origin or construction.
// β NOT DETECTED - Value from import
import { getValue } from './helpers';
processValue(getValue()); // Cross-file not trackedMitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.
Use this rule
npm install -D oxlint-plugin-reliability{ "jsPlugins": ["oxlint-plugin-reliability"], "rules": { "reliability/no-await-in-loop": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-reliability'], rules: { 'reliability/no-await-in-loop': 'error', }, },});oxlint-plugin-reliability re-exports eslint-plugin-reliability/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it β or follow the AI-code-security benchmarks behind these rules.