Skip to main content
interlace
Plugin: reliabilityRules

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

AspectDetails
SeverityWarning (performance)
Auto-Fix❌ No (suggests refactoring patterns)
CategoryQuality
ESLint MCPβœ… Optimized for ESLint MCP integration
Best ForPerformance-critical applications, API batch operations

Rule Details

Why This Matters

IssueImpactSolution
⚑ PerformanceN*latency instead of 1xUse Promise.all()
πŸ”„ Sequential BottleneckBlocks event loopConcurrent execution
πŸ“Š ScalabilitySlow with large datasetsBatch processing
🎯 Resource UsageInefficient API callsParallelization

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 await in a slot evaluated once before iterating (the for…of / for…in iterable, a for initialiser);
  • an await inside 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 a break or return. Those iterations cannot run concurrently, so the sequential loop is the correct shape.

Configuration

OptionTypeDefaultDescription
allowForOfbooleanfalseAllow await in for-of loops
allowWhilebooleanfalseAllow await in while and do-while loops
checkConcurrencybooleantrueShapes 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

ScenarioRecommendation
πŸ“Š Order-dependent opsUse allowForOf: true or disable rule
πŸ”’ Rate-limited APIsUse controlled concurrency libraries
πŸ’Ύ Memory constraintsSequential may be necessary
πŸ”„ Transaction chainsSequential execution required

Comparison with Alternatives

Featureno-await-in-loopeslint built-inunicorn
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

Further Reading

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 tracked

Mitigation: 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 tracked

Mitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.

Use this rule

npm install -D oxlint-plugin-reliability
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-reliability"],  "rules": {    "reliability/no-await-in-loop": "error"  }}
vite.config.ts
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.