npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eslint-plugin-parallelize

v0.3.0

Published

ESLint rule that flags sequential awaits with no data dependency between them, and auto-fixes them into maximally parallel Promise.all groupings. Type-aware when type information is available.

Readme

eslint-plugin-parallelize

CI npm

An ESLint rule that finds awaits that run sequentially but don't depend on each other, and auto-fixes them into maximally parallel Promise.all groupings.

// before
const user = await fetchUser();
const posts = await fetchPosts();
const enriched = await enrich(user, posts);

// after --fix
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
const enriched = await enrich(user, posts);

The rule assumes your async operations are side-effect free with respect to each other — i.e. it is semantically safe to start them in any order. That assumption is what makes the rewrite sound; if your codebase relies on cross-call effect ordering, don't enable the auto-fix blindly.

Install

npm install --save-dev eslint-plugin-parallelize

Usage (flat config)

// eslint.config.js
import parallelize from 'eslint-plugin-parallelize';

export default [
  {
    plugins: { parallelize },
    rules: {
      'parallelize/no-sequential-await': 'warn',
    },
  },
];

Or use the bundled config: parallelize.configs.recommended.

How it works

Statement runs

The rule scans each block for maximal runs of consecutive await statements in these forms:

await foo();              // expression
const x = await foo();    // declaration (any destructuring pattern)
x = await foo();          // assignment (incl. obj.prop = await foo())

Within a run it builds a data-dependency graph using ESLint's scope analysis — identifier references are resolved to their actual variables, so shadowing, closures, destructuring defaults, and callback parameters are handled exactly. Statement B depends on statement A when any of:

  • read-after-write — B reads a variable A writes (const a = await f(); await g(a))
  • write-after-read — B writes a variable A reads
  • write-after-write — B writes a variable A writes

Property writes (obj.x = await f()) are conservatively treated as writes to obj. Untrackable targets (this.x, f().x) make the statement an ordering barrier.

Statements are then layered by longest path in the dependency graph. Any layer with two or more statements is available parallelism, and the layering is the fix:

// before: a ─→ b, a ─→ c, {b,c} ─→ d
const a = await f();
const b = await g(a);
const c = await h(a);
const d = await i(b, c);

// after --fix
const a = await f();
const [b, c] = await Promise.all([g(a), h(a)]);
const d = await i(b, c);

Statements in the same layer may be reordered relative to dependent statements between them — this is safe exactly because the dependency analysis proved them independent.

Boolean logic

Short-circuit operators serialize awaits too:

const ok = await isAdmin(user) && await hasQuota(user);

hasQuota doesn't start until isAdmin resolves. With side-effect-free operands the parallel form is equivalent (&&/||/?? become plain boolean combination once both values are in hand):

const [admin, quota] = await Promise.all([isAdmin(user), hasQuota(user)]);
const ok = admin && quota;

The rule reports multi-await &&/||/?? trees (message only — the rewrite needs new bindings, so it isn't auto-fixed). Expressions where one operand's assignment feeds another ((x = await f()) && await g(x)) are left alone.

Type-aware mode (automatic)

The rule is fully functional syntactically on any JS/TS codebase. When it runs under @typescript-eslint/parser with type information enabled, it upgrades automatically — no separate rule, no option:

// eslint.config.js
import tseslint from 'typescript-eslint';
import parallelize from 'eslint-plugin-parallelize';

export default tseslint.config(
  { languageOptions: { parserOptions: { projectService: true } } },
  parallelize.configs.recommended,
);

With types available:

  • Un-awaited promise declarations stop being barriers. const p = fetchB(); between two awaits currently splits the run; with types the rule knows p is a thenable already in flight, analyzes it for dependencies like any other statement, and can group awaits across it:

    // before
    const a = await fetchA();
    const p = fetchB();
    const b = await fetchC();
    
    // after --fix
    const p = fetchB();
    const [a, b] = await Promise.all([fetchA(), fetchC()]);
  • Awaits of sync-typed values no longer count as async work. await parse(x); await parse(y); starts nothing asynchronous — with types it is not flagged (grouping would gain nothing).

  • any/unknown keep the syntactic behavior, so untyped code is never silently under-reported.

Loops

An await inside a loop serializes across iterations. Unlike core no-await-in-loop, this rule only reports when the serialization is unnecessary — when neither the await's inputs nor the decision to keep looping depend on results from previous iterations:

for (const item of items) {
  await process(item);          // reported: iterations are independent
}

let token;
do {
  const page = await fetchPage(token);   // not reported: loop-carried
  token = page.next;                     // dependency via the paging token
} while (token);

This works via a taint pass: variables written from await results are tainted, taint propagates through assignments to a fixpoint, and the loop is left alone when any of these hold:

  • a reported await's inputs read tainted state (paging, folds, pointer chases)
  • the loop condition reads tainted state or itself awaits (retry, polling)
  • a break/continue/return/throw is guarded by a tainted condition (poll-until-done)
  • the loop is while (true)/for (;;) (daemons), for await ... of, or contains an untrackable property write (this.x = ...)
  • the loop only awaits already-in-flight promises

Loop findings are report-only (rewriting to map + Promise.all changes structure too much to auto-fix safely). If you enable this rule, disable core no-await-in-loop — this is a dependency-aware superset of the cases worth flagging.

When parallelizing wouldn't help

Awaiting an already in-flight promise sequentially costs nothing — the work is already running:

await p1;   // p1 was started earlier
await p2;   // fine: total time is max(p1, p2) either way

The rule only reports when some await after the first in its group actually starts new work (contains a call, new, tagged template, or dynamic import()). So await p1; await foo(); is reported (foo could have started immediately), but await foo(); await p1; is not.

When the fix is withheld

The rule still reports, but won't auto-fix, when a faithful single-statement rewrite doesn't exist:

  • assignments to existing bindings (x = await f()) — would need to invent temporaries
  • mixed const/let in one group
  • comments between the statements (they'd be destroyed)

Options

{
  "parallelize/no-sequential-await": [
    "warn",
    { "ignoreTry": false, "checkLoops": true, "requireConsumedResult": false }
  ]
}
  • ignoreTry (default false) — skip blocks directly inside try/catch/finally. Promise.all is fail-fast and starts every operation regardless of which one throws, which can matter for carefully staged error handling even without side effects.

  • checkLoops (default true) — report loops whose awaits have no loop-carried dependency. Set to false if you prefer core no-await-in-loop's blunter behavior (or no loop checking at all).

  • requireConsumedResult (default false) — only flag when every awaited operation's result is consumed (assigned or destructured to a real binding). A run containing any statement-level await foo(); — or a declaration/assignment that binds nothing, e.g. const {} = await foo() or an all-holes const [,] = await foo() — is left alone; likewise a loop whose awaited result is discarded.

    A discarded (void) result is a strong signal the call is there for its side-effect (a mutation, cache invalidation, ordered delete, message send), which is exactly where hidden ordering/data dependencies that this analysis can't see tend to live — await create(...); await refetch(...), await mutate(...); await invalidate(...), bottom-up deletes, and so on. Awaits whose results are read back are far more often genuine independent loads (data fetches, fetchPage, dynamic import()) that parallelize safely.

    This is a conservative, opt-in heuristic: it can suppress a legitimately-parallelizable run whose reads happen to discard their results. Enable it when you want the rule biased hard toward safe suggestions on side-effect-heavy code.

Semantics changed by the fix

Even with side-effect-free operations, two observable differences exist:

  1. Failure timing/selection: sequentially, a rejection in the first await prevents later calls from ever starting. After the fix, all calls start; Promise.all rejects with the first rejection to settle. (Promise.all does subscribe to every promise, so no unhandled-rejection warnings.)
  2. Resource pressure: N concurrent calls instead of 1 at a time (connection pools, rate limits).

Both are almost always acceptable — and usually desirable — under the no-side-effects assumption, but it's why ignoreTry exists.

Limitations (v0.1)

  • Statement-level only: export const a = await f(), multi-declarator statements, return await f(), and statements containing nested awaits (await f(await g())) act as barriers rather than being analyzed.
  • No alias analysis: a.x = await f(); b.y = await g() is considered independent when a and b are distinct variables, even if they alias the same object.
  • No interprocedural analysis: a helper that internally awaits sequentially won't be seen.
  • Without type information, promise detection is purely syntactic: un-awaited promise-typed declarations act as run barriers, and awaits of sync values are treated as async work. Enable projectService (see type-aware mode) to lift both.
  • Ternaries (await c() ? x : y) are out of scope for now.
  • Loop taint analysis is variable-level, not flow-sensitive (no SSA): a variable tainted anywhere in the loop is tainted everywhere, which errs toward silence.

Prior art

Nothing existing does dependency-aware detection of serializable awaits:

License

MIT