Claude Cursor GitHub Copilot Skill

javascript-runtime-async-review

Review JavaScript/TypeScript for event-loop and microtask/macrotask ordering correctness, unhandled Promise rejection paths, DOM event-listener and timer cleanup, and race-condition risk in rapid-repeated-async UI patterns, tracing actual browser scheduling semantics rather than

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download vincentchuwaichow-vanguard-frontier-agentic-skills_frontend_javascript-runtime-async-review-febe32a.zip · 12 KB
Part of vincentchuwaichow/vanguard-frontier-agentic — 293 skills

Install

skills CLI npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/javascript-runtime-async-review
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
Git git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

JavaScript Runtime & Async Correctness Review

Purpose

async/await reads like synchronous code, which leads developers to reason about it as if it were synchronous — but the underlying microtask/macrotask scheduling model still governs actual execution order, and getting it wrong produces exactly the class of bug that's hardest to catch in normal testing: intermittent, timing-dependent, "works on my machine" race conditions. This skill traces real event-loop ordering, audits every async chain for unhandled-rejection paths, and verifies every listener/timer has a reachable cleanup path, so timing correctness is verified rather than assumed.

When to use

Use this skill when the user asks to:

  • review JavaScript/TypeScript async code (Promises, async/await, timers) for correctness,
  • audit event-listener, timer, or observer cleanup to prevent memory leaks,
  • diagnose or prevent race conditions in UI patterns with rapid repeated async calls (search-as-you-type, polling, infinite scroll),
  • check for unhandled Promise rejections, especially on security-relevant code paths,
  • verify actual microtask/macrotask execution order for a specific code sequence.

Do not use this skill for:

  • framework-specific effect/hook lifecycle review (React useEffect dependency arrays, Vue watchers) — use the matching framework-specific review skill; this skill covers the underlying runtime/scheduling layer those skills build on,
  • bundler/build-tool configuration or transpilation-target correctness — out of scope,
  • a claim that requires live production traffic or load-test evidence to confirm timing under real network jitter — this is static-review-only; label those findings as needing live verification, do not assert them as proven.

Context7 Documentation Protocol

  • Resolve the docs source with resolve-library-id against /mdn/content (or the closest current MDN Context7 ID) before asserting any ordering, scheduling, or API-behavior claim.
  • Before ruling on a specific ordering question (e.g., "does this setTimeout(fn, 0) run before or after this .then()"), call query-docs for that exact scenario — do not answer from memory or from a synchronous mental model, since ordering intuitions are a frequent source of subtly-wrong review comments.
  • Trace microtask-vs-task distinctions explicitly: microtasks (Promise callbacks, queueMicrotask, async/await continuations) drain completely — including microtasks they themselves enqueue — before the next macrotask (setTimeout, setInterval, I/O, UI rendering) runs. Cite this distinction rather than assuming the reader already applies it correctly.
  • For cancellation/lifecycle patterns (AbortController, removeEventListener), verify current API shape and browser support notes via query-docs before recommending a specific signature — do not invent options or assume universal support without checking.
  • If Context7 is unavailable, fall back to the official_docs URLs in this skill's metadata.json and label the claim documentation-based, unverified against current release.

Lean operating rules

  • Do not assume async/await makes code race-condition-safe by default — it only guarantees ordering within a single linear await chain, not between independently-triggered async operations.
  • Trace the actual microtask/macrotask interleaving for any ordering claim rather than asserting it from a synchronous mental model; query current MDN event-loop/microtask docs before ruling, since this is a frequent source of subtly-wrong assumptions.
  • Every Promise chain must terminate in a .catch or sit inside a try/catch around every await — flag any chain that doesn't as an unhandled-rejection risk, with extra weight if it touches an authorization/permission check.
  • Every addEventListener/setInterval/non-one-shot setTimeout/Observer must be matched to a traced, reachable removeEventListener/clearInterval/clearTimeout/disconnect call, including on early-return and error paths.
  • Any UI pattern with rapid repeated async calls (search-as-you-type, polling) must use AbortController, a request-generation counter, or equivalent sequencing — flag its absence as a race-condition risk, not a style preference.
  • Do not accept "passed manual testing" as sufficient evidence for timing-sensitive code — manual testing rarely hits the interleavings that matter; flag as needing live/load-tested verification instead.
  • Flag eval, new Function(), and string-argument timers as code-injection risks requiring explicit justification, not routine patterns.
  • Label every ordering/timing claim as spec-traced, documentation-based, or needs live-runtime verification so reviewers know what's actually been verified.
  • Never execute, build, or run application code as part of this review; this is a static-review skill (Read/Grep/Glob only, plus git diff for scoping and WebFetch for spec/docs grounding).

References

Load these only when needed:

Response minimum

Return, at minimum:

  • the event-loop/ordering verdict for each async chain reviewed, with the traced resolution order (not assumed),
  • the unhandled-rejection audit result for every Promise chain in scope,
  • the listener/timer/observer cleanup audit result, matching every registration to its cleanup call,
  • race-condition risk flags for any rapid-repeated-call pattern lacking sequencing or cancellation,
  • residual risk notes for anything requiring live or load-tested verification beyond this static trace.
Files (vanguard-frontier-agentic)
  • references
    • event-loop-tracing.md 6.3 KB
      # Event-Loop Ordering Trace Patterns
      
      Use this reference only when a review requires tracing the actual resolution order of a specific microtask/macrotask/`await` sequence — not for general async-code review; use `rejection-audit.md` or `race-condition-patterns.md` for those.
      
      ## What people get wrong
      
      The common bad assumption is:
      
      > "`async`/`await` reads top-to-bottom, so it executes top-to-bottom relative to everything else."
      
      That is wrong. `async`/`await` is sugar over Promises. Every `await` suspends the async function and schedules its continuation as a **microtask**; it does not pause the rest of the program. Code after the async function call keeps running synchronously until the call stack empties, and only then does the microtask queue drain — completely, including any new microtasks enqueued during that drain — before the next macrotask (`setTimeout`, `setInterval`, I/O callback, UI paint) runs.
      
      ## Officially grounded ordering rules (MDN)
      
      Two queue classes, not one:
      
      - **Task queue (macrotasks):** initial script execution, `setTimeout`/`setInterval` callbacks, event dispatch, I/O. When a new event-loop iteration begins, the runtime executes exactly the next task from the task queue. Tasks queued during that iteration wait for the *next* iteration.
      - **Microtask queue:** Promise `.then`/`.catch`/`.finally` callbacks, `async`/`await` continuations, `queueMicrotask()`. Whenever a task exits and the call stack is empty, **all** microtasks run in turn — including ones newly enqueued by currently-running microtasks — until the microtask queue is fully empty. Only then does the next macrotask run.
      
      Canonical worked example (MDN, `Guide/Using_promises`):
      
      ```js
      const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
      
      wait(0).then(() => console.log(4));
      Promise.resolve()
        .then(() => console.log(2))
        .then(() => console.log(3));
      console.log(1);
      // Output: 1, 2, 3, 4
      ```
      
      `wait(0)` still goes through `setTimeout`, so its `.then` callback is a macrotask-queued continuation — it runs *after* all currently-queryable microtasks, even though its delay is `0` and even though it was scheduled first in source order. Do not accept "it's `setTimeout(fn, 0)` so it's basically synchronous" as a review claim; it is not.
      
      Second canonical example, showing microtasks interleaving with `await` continuations (MDN, `Reference/Operators/await`):
      
      ```js
      let i = 0;
      queueMicrotask(function test() {
        i++;
        console.log("microtask", i);
        if (i < 3) queueMicrotask(test);
      });
      
      (async () => {
        console.log("async function start");
        for (let i = 1; i < 3; i++) {
          await null;
          console.log("async function resume", i);
        }
        await null;
        console.log("async function end");
      })();
      
      queueMicrotask(() => console.log("queueMicrotask() after calling async function"));
      console.log("script sync part end");
      
      // Logs, in order:
      // async function start
      // script sync part end
      // microtask 1
      // async function resume 1
      // queueMicrotask() after calling async function
      // microtask 2
      // async function resume 2
      // microtask 3
      // async function end
      ```
      
      The load-bearing detail: each `await null` inside the async function re-enters the *back* of the microtask queue on resume, so it interleaves with other pending microtasks rather than running immediately after the previous line. A review claim that "the loop finishes before the other microtasks run" is wrong given this trace — verify against the actual queue order, don't assert it.
      
      ## Non-negotiable design rules
      
      1. **Never assert an ordering claim without naming which queue each operation lands on.** "This runs first" is not a finding; "this is a microtask (Promise `.then`) so it runs before that macrotask (`setTimeout`), regardless of the `setTimeout` delay value" is.
      2. **`setTimeout(fn, 0)` (or any small delay) is not "basically synchronous" and is not "basically a microtask."** It is a macrotask. All pending microtasks — including ones enqueued while draining the current batch — run before it, no matter how small the delay.
      3. **A `for`/`while` loop containing multiple `await` points interleaves with other queued microtasks on every resume**, not just at the start and end of the loop. Do not assume the loop runs to completion "in one go" once started.
      4. **`Promise.resolve().then(...)` chains queue one microtask per `.then` link.** A five-link `.then` chain takes five microtask-queue drains to fully resolve, and other pending microtasks interleave between each link if they were already queued.
      5. **Rendering/painting is a macrotask-adjacent checkpoint, not a microtask checkpoint.** Layout/paint work happens after the microtask queue drains and before the next macrotask in browsers implementing the HTML spec's rendering opportunity model — do not assume a DOM mutation made inside a microtask is guaranteed visible to the user before the *next* microtask runs; it is not guaranteed until a rendering opportunity occurs.
      
      ## Verification targets
      
      When repo evidence is available, verify a disputed ordering claim by:
      
      - identifying every `Promise`-returning call, `.then`/`.catch`/`.finally`, `await`, and `queueMicrotask` in the sequence and labeling each a microtask source,
      - identifying every `setTimeout`/`setInterval`/event-dispatch/I-O callback and labeling each a macrotask source,
      - walking the sequence in source order, applying "drain all microtasks (including newly enqueued ones) before the next macrotask" at each synchronous-execution boundary,
      - if the trace is non-obvious or contested, recommend the reviewer actually run the snippet (`node` REPL or browser console) rather than settle the dispute by further reasoning alone — label the resulting claim `needs live-runtime verification` until that's done.
      
      ## When to push back
      
      Push back if the user asks you to:
      
      - assert an ordering claim "because `async`/`await` looks synchronous" without tracing the actual queue mechanics — that is exactly the intuition that produces production race conditions,
      - treat `setTimeout(fn, 0)` as a synchronization primitive to "make sure the DOM update happened" — it is not deterministic relative to other macrotasks/microtasks queued by other code and is not a documented ordering guarantee,
      - skip tracing a "probably fine" reordering in a diff without walking the actual microtask/macrotask sequence — "probably fine" is not evidence for a scheduling claim.
      
    • race-condition-patterns.md 7.6 KB
      # Race-Condition and Cancellation Patterns
      
      Use this reference only when reviewing a UI pattern with rapid repeated async calls (search-as-you-type, polling, infinite scroll, tab-switch-triggered refetch, debounced/throttled handlers) for `AbortController`, generation-counter, or equivalent sequencing needs — not for general rejection-handling audit (`rejection-audit.md`) or pure ordering questions with no repeated-call risk (`event-loop-tracing.md`).
      
      ## What people get wrong
      
      The common bad assumption is:
      
      > "The requests are sent in order, so the responses will arrive and get applied in order too."
      
      That is false, and it is the single most common source of "stale data flashes onto the screen" bug reports. Nothing about the network or the event loop guarantees that responses resolve in request order — a slower request issued first can resolve *after* a faster request issued second, and if both unconditionally call the same `setState`/DOM-write on resolution, the stale (first-issued, slower) response can overwrite the fresh (second-issued, faster) one. This is not a rare edge case; it is a predictable consequence of variable network latency and is trivially reproducible by throttling one request in DevTools.
      
      ## Officially grounded cancellation pattern (MDN)
      
      `AbortController`/`AbortSignal` is the documented mechanism for cancelling `fetch()` and for auto-removing event listeners, and it is the primary tool for closing this class of race condition — it actually cancels the in-flight request/listener rather than merely ignoring its eventual result.
      
      Cancelling `fetch()`:
      
      ```js
      const controller = new AbortController();
      
      async function search(query) {
        controller.abort(); // cancel any prior in-flight search
        const signal = controller.signal;
        try {
          const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
          const results = await response.json();
          renderResults(results);
        } catch (err) {
          if (err.name === "AbortError") return; // expected: a newer request superseded this one
          renderError(err);
        }
      }
      ```
      
      Note the bug in the naive version of this pattern: reusing a single `controller` across calls means `controller.abort()` must create a **new** controller for the next request (an already-aborted controller's signal cannot be un-aborted). The corrected shape keeps the controller reference in an outer/module/component-instance scope and reassigns it on every new call:
      
      ```js
      let currentController = null;
      
      async function search(query) {
        currentController?.abort();
        currentController = new AbortController();
        const { signal } = currentController;
        try {
          const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal });
          renderResults(await response.json());
        } catch (err) {
          if (err.name === "AbortError") return;
          renderError(err);
        }
      }
      ```
      
      Auto-removing an event listener with the same signal, instead of a manual `removeEventListener` call (MDN, `EventTarget/addEventListener`):
      
      ```js
      const controller = new AbortController();
      el.addEventListener("click", handler, { signal: controller.signal });
      // Later, one call removes this (and any other listener sharing the signal):
      controller.abort();
      ```
      
      ## Non-negotiable design rules
      
      1. **Every rapid-repeated-call site needs one of: `AbortController` cancellation, a request-generation counter, or a documented equivalent (e.g., a library-level cancellation/dedup mechanism like a query library's built-in request deduplication).** Absence of any of these on a search-as-you-type, poll, or tab-switch-refetch pattern is a race-condition finding, not a style note — regardless of how unlikely the reviewer judges the specific timing to be in practice.
      2. **A generation-counter guard must increment *before* issuing the new request and must be checked *immediately before* the state-mutating call on resolution**, comparing against the value captured at issue-time — not the current value read again at resolution-time (that comparison is always true and guards nothing):
         ```js
         let latestRequestId = 0;
         async function search(query) {
           const requestId = ++latestRequestId;
           const results = await fetchResults(query);
           if (requestId !== latestRequestId) return; // a newer request superseded this one
           renderResults(results);
         }
         ```
      3. **`AbortController.abort()` rejects the pending `fetch()` Promise with an `AbortError`** — the catch/rejection path must explicitly distinguish `AbortError` (expected, safe to silently return) from every other error (must still surface to the user or logging). Treat a catch block that swallows *all* errors identically, including genuine network/server failures, as a separate finding from the missing-cancellation finding — silencing real errors alongside expected aborts hides genuine outages.
      4. **Debounce/throttle alone does not fix this class of race.** Debouncing reduces how many requests are *issued*; it does not guarantee the *responses* resolve in issue order. A debounced search-as-you-type handler still needs cancellation or a generation guard on the requests it does issue.
      5. **Polling intervals need the same cancellation discipline as one-shot requests**, plus `clearInterval`/`clearTimeout` on unmount/teardown — an in-flight poll response arriving after teardown that still writes to now-stale UI state or a detached DOM node is the same defect class as the search-as-you-type case, compounded by the interval continuing to fire if not cleared.
      6. **Session/identity switches (logout, account switch, tab becomes a different user's session) are the highest-severity variant of this pattern.** A stale in-flight request for the previous session resolving after the switch and writing its response into the new session's view is a data-exposure defect (one user's data rendered into another user's session), not merely a UI glitch — treat this specific trigger sequence as HIGH severity by default.
      
      ## Verification targets
      
      When repo evidence is available, verify a race-condition finding by:
      
      - confirming the actual trigger sequence with concrete inputs (e.g., "type 'a', then quickly type 'ab' — if the `/search?q=a` response is slower than `/search?q=ab`, it can overwrite the correct results" ) rather than asserting a race "could" happen without describing how,
      - checking whether the underlying HTTP client actually supports `signal` (native `fetch` does; some wrapped/legacy clients require an adapter or don't support cancellation at all — verify before recommending `AbortController` as the fix, and recommend a generation-counter guard instead when the client can't cancel),
      - checking whether a `try`/`catch` around an aborted request correctly special-cases `AbortError` (rule 3 above) — a fix that adds cancellation but treats the resulting `AbortError` as a real failure introduces a new (spurious error UI) bug.
      
      ## When to push back
      
      Push back if the user asks you to:
      
      - "just debounce it more" as the fix for a reported stale-data race — longer debounce reduces frequency, not the underlying unordered-resolution risk, and adds latency without closing the bug,
      - skip cancellation because "the backend is fast, this basically never happens" — network latency variance (not backend speed alone) drives this bug, and it is exactly the kind of intermittent, hard-to-reproduce defect that's expensive once it reaches production; treat "basically never happens" as unverified until shown otherwise,
      - add a generation counter or `AbortController` but continue writing state before checking the guard — the guard must be the last check *before* the state-mutating call, not merely present somewhere earlier in the function.
      
    • rejection-audit.md 6.8 KB
      # Unhandled-Rejection Audit Checklist
      
      Use this reference only when systematically auditing a file/module's Promise chains and `async` functions for missing `.catch`/try-catch coverage — not for tracing execution order (`event-loop-tracing.md`) or for cancellation/sequencing patterns (`race-condition-patterns.md`).
      
      ## What people get wrong
      
      The common bad assumption is:
      
      > "The function is `async`, so any error inside it becomes a normal thrown error the caller will see."
      
      That is incomplete. An `async` function that throws or whose `await`ed Promise rejects returns a **rejected Promise**, not a synchronous throw. If nothing consumes that rejection — no `.catch`, no surrounding `try`/`catch` at an `await` call site, no `await` at all on a fire-and-forget call — the rejection surfaces only as an "Unhandled promise rejection" warning (or, in older/misconfigured environments, silently), and any logic gated on that path (an authorization check, a save confirmation, a UI state update) simply never runs. A rejected permission check that isn't awaited/caught can fail open: the calling code proceeds past the check as if nothing happened, because nothing ever observed the rejection.
      
      ## Officially grounded pattern
      
      MDN's Promise/`await` docs converge on one rule: every Promise-producing expression needs exactly one of these along every path that can reject:
      
      - a `.catch(...)` (or `.then(onFulfilled, onRejected)`) attached to the chain, or
      - an enclosing `try { await ... } catch (e) { ... }` around the `await`.
      
      ```js
      // Unhandled: no .catch, and the caller does not await it.
      function saveDraft(draft) {
        api.save(draft).then((res) => showSavedToast(res));
      }
      
      // Handled: explicit .catch covers the whole chain.
      function saveDraft(draft) {
        api
          .save(draft)
          .then((res) => showSavedToast(res))
          .catch((err) => showErrorToast(err));
      }
      
      // Handled: try/catch around the await.
      async function saveDraft(draft) {
        try {
          const res = await api.save(draft);
          showSavedToast(res);
        } catch (err) {
          showErrorToast(err);
        }
      }
      ```
      
      ## Non-negotiable design rules
      
      1. **Every Promise chain must terminate in exactly one rejection-handling construct along every branch.** A `.then(...).then(...)` chain with no trailing `.catch` is a finding even if an earlier link in the chain "usually" succeeds — "usually" is not "always," and the whole point of the audit is the failure path.
      2. **A `try` block around a call that does *not* `await` its Promise-returning expression does not catch that Promise's rejection.** `try { somePromiseFn(); } catch (e) {}` only catches a *synchronous* throw during the call setup, not an asynchronous rejection from the returned Promise. Flag this exact pattern by name — it is a common false sense of coverage.
      3. **A "fire-and-forget" call (`doSomethingAsync()` with no `await`, no `.then`, no `.catch`) is an unhandled-rejection risk by default.** It is only acceptable when the function is documented/verified to never reject (rare) or when the codebase has a deliberate, reviewed pattern for intentionally-detached tasks (e.g., a wrapper that logs-and-swallows). Do not accept "it's just a fire-and-forget analytics call" as an exemption without checking whether the underlying call can actually reject (network calls almost always can).
      4. **`Promise.all`/`Promise.allSettled`/`Promise.race`/`Promise.any` each have different rejection semantics — verify which one is in use before asserting the rejection is handled.** `Promise.all` rejects as soon as any input rejects (and the other results are dropped, not just delayed); `Promise.allSettled` never rejects and instead requires the caller to inspect each `status: "rejected"` result individually — a `.catch` on an `.allSettled` chain never fires for individual-item failures, so per-item error handling must happen inside the mapping/consuming code, not assumed to exist because a `.catch` is present elsewhere in the chain.
      5. **Authorization/permission-check code paths get elevated severity, not routine severity, for a missing rejection handler.** If a rejected check silently fails to block the gated action (because nothing awaited/caught it and the surrounding code proceeds unconditionally), that is a fail-open security defect, not a UX polish item.
      
      ## Severity escalation
      
      Treat a missing rejection handler as **HIGH severity** when:
      
      - the Promise chain gates an authorization, permission, or entitlement check — a rejection that isn't observed means the gate is bypassed by default (fail-open),
      - the Promise chain performs an authenticated write/mutation and the caller has no way to know it failed — silent data loss or an inconsistent state the user believes succeeded,
      - the rejection would otherwise crash a Node.js process (unhandled rejections terminate the process by default in modern Node major versions) — verify the target runtime's current behavior via `query-docs` rather than assuming a specific Node version's default.
      
      Otherwise (a rejection that only affects a non-critical UI affordance, like a tooltip's optional prefetch), MEDIUM or LOW is appropriate depending on user-visible impact — but still a finding, not a non-issue.
      
      ## Verification targets
      
      When repo evidence is available, verify each finding against:
      
      - whether a global handler exists (`window.addEventListener("unhandledrejection", ...)` in browsers, `process.on("unhandledRejection", ...)` in Node) — a global handler changes the blast radius (it prevents a silent failure) but does **not** fix the underlying logic gap (the gated action still ran unguarded); note both facts, do not treat the global handler as sufficient remediation for a specific chain,
      - whether the function is exported/public API — an unhandled rejection inside a shared utility has a larger blast radius than one confined to a single component's internal helper,
      - the actual runtime target (browser vs. Node, and which Node major) before asserting process-crash consequences, since default unhandled-rejection behavior has changed across Node versions.
      
      ## When to push back
      
      Push back if the user asks you to:
      
      - add a blanket top-level `unhandledrejection`/`unhandledRejection` listener as the fix instead of adding scoped `.catch`/`try-catch` at the actual call sites — a global listener can suppress the crash/warning without fixing the fail-open logic gap underneath it,
      - treat `.catch(() => {})` (swallow-and-ignore) as equivalent to proper error handling for an authorization or mutation path — silently swallowing the error is a different (and often worse) defect than the original unhandled rejection, because it removes the warning signal without adding a safe fallback,
      - skip the audit on a chain because "it basically never fails in practice" — that is exactly the assumption this audit exists to challenge; require either evidence the operation cannot reject, or a rejection handler.
      
  • metadata.json 1.7 KB
    {
      "id": "javascript-runtime-async-review",
      "name": "JavaScript Runtime & Async Correctness Review",
      "type": "skill",
      "provider": "frontend",
      "harnesses": [
        "claude-code",
        "cursor",
        "codex",
        "gemini",
        "kiro",
        "other"
      ],
      "summary": "Reviews JavaScript for event-loop/microtask ordering correctness, unhandled Promise rejections, DOM event-listener lifecycle, and race-condition risk in rapid-repeated-async UI patterns, tracing actual browser scheduling behavior rather than assumed synchronous-style reasoning.",
      "source_type": "original",
      "official_docs": [
        "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises",
        "https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide",
        "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await",
        "https://html.spec.whatwg.org/multipage/webappapis.html#event-loops",
        "https://developer.mozilla.org/en-US/docs/Web/API/AbortController",
        "https://tc39.es/ecma262/"
      ],
      "security_notes": "Flag unhandled Promise rejections on authorization/permission-check code paths — a rejected check that isn't awaited/caught can fail open. Flag eval, new Function(), and string-argument setTimeout/setInterval as code-injection surfaces. Flag window/document message-event listeners without an origin check on postMessage payloads. Require AbortController-based cancellation for lifecycle-bound fetches to prevent stale-response race conditions that can leak one user's data into another user's view after a fast session switch.",
      "last_verified": "2026-07-02",
      "path": "skills/frontend/javascript-runtime-async-review",
      "author": "github: VincentChuWaiChow",
      "version": "0.1.0"
    }
    
  • SKILL.md 6.5 KB
    ---
    name: javascript-runtime-async-review
    description: Review JavaScript/TypeScript for event-loop and microtask/macrotask ordering correctness, unhandled Promise rejection paths, DOM event-listener and timer cleanup, and race-condition risk in rapid-repeated-async UI patterns, tracing actual browser scheduling semantics rather than assumed synchronous-style reasoning about async code.
    allowed-tools: Read Grep Glob Bash(git diff:*) WebFetch
    metadata:
      author: "github: VincentChuWaiChow"
      version: "0.1.0"
      updated: "2026-07-02"
      category: delivery
    ---
    
    # JavaScript Runtime & Async Correctness Review
    
    ## Purpose
    
    `async`/`await` reads like synchronous code, which leads developers to reason about it as if it were synchronous — but the underlying microtask/macrotask scheduling model still governs actual execution order, and getting it wrong produces exactly the class of bug that's hardest to catch in normal testing: intermittent, timing-dependent, "works on my machine" race conditions. This skill traces real event-loop ordering, audits every async chain for unhandled-rejection paths, and verifies every listener/timer has a reachable cleanup path, so timing correctness is verified rather than assumed.
    
    ## When to use
    
    Use this skill when the user asks to:
    
    - review JavaScript/TypeScript async code (Promises, async/await, timers) for correctness,
    - audit event-listener, timer, or observer cleanup to prevent memory leaks,
    - diagnose or prevent race conditions in UI patterns with rapid repeated async calls (search-as-you-type, polling, infinite scroll),
    - check for unhandled Promise rejections, especially on security-relevant code paths,
    - verify actual microtask/macrotask execution order for a specific code sequence.
    
    Do not use this skill for:
    
    - framework-specific effect/hook lifecycle review (React `useEffect` dependency arrays, Vue watchers) — use the matching framework-specific review skill; this skill covers the underlying runtime/scheduling layer those skills build on,
    - bundler/build-tool configuration or transpilation-target correctness — out of scope,
    - a claim that requires live production traffic or load-test evidence to confirm timing under real network jitter — this is static-review-only; label those findings as needing live verification, do not assert them as proven.
    
    ## Context7 Documentation Protocol
    
    - Resolve the docs source with `resolve-library-id` against `/mdn/content` (or the closest current MDN Context7 ID) before asserting any ordering, scheduling, or API-behavior claim.
    - Before ruling on a specific ordering question (e.g., "does this `setTimeout(fn, 0)` run before or after this `.then()`"), call `query-docs` for that exact scenario — do not answer from memory or from a synchronous mental model, since ordering intuitions are a frequent source of subtly-wrong review comments.
    - Trace microtask-vs-task distinctions explicitly: microtasks (Promise callbacks, `queueMicrotask`, `async`/`await` continuations) drain completely — including microtasks they themselves enqueue — before the next macrotask (`setTimeout`, `setInterval`, I/O, UI rendering) runs. Cite this distinction rather than assuming the reader already applies it correctly.
    - For cancellation/lifecycle patterns (`AbortController`, `removeEventListener`), verify current API shape and browser support notes via `query-docs` before recommending a specific signature — do not invent options or assume universal support without checking.
    - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based, unverified against current release`.
    
    ## Lean operating rules
    
    - Do not assume `async`/`await` makes code race-condition-safe by default — it only guarantees ordering within a single linear await chain, not between independently-triggered async operations.
    - Trace the actual microtask/macrotask interleaving for any ordering claim rather than asserting it from a synchronous mental model; query current MDN event-loop/microtask docs before ruling, since this is a frequent source of subtly-wrong assumptions.
    - Every Promise chain must terminate in a `.catch` or sit inside a try/catch around every `await` — flag any chain that doesn't as an unhandled-rejection risk, with extra weight if it touches an authorization/permission check.
    - Every `addEventListener`/`setInterval`/non-one-shot `setTimeout`/`Observer` must be matched to a traced, reachable `removeEventListener`/`clearInterval`/`clearTimeout`/`disconnect` call, including on early-return and error paths.
    - Any UI pattern with rapid repeated async calls (search-as-you-type, polling) must use `AbortController`, a request-generation counter, or equivalent sequencing — flag its absence as a race-condition risk, not a style preference.
    - Do not accept "passed manual testing" as sufficient evidence for timing-sensitive code — manual testing rarely hits the interleavings that matter; flag as needing live/load-tested verification instead.
    - Flag `eval`, `new Function()`, and string-argument timers as code-injection risks requiring explicit justification, not routine patterns.
    - Label every ordering/timing claim as `spec-traced`, `documentation-based`, or `needs live-runtime verification` so reviewers know what's actually been verified.
    - Never execute, build, or run application code as part of this review; this is a static-review skill (Read/Grep/Glob only, plus `git diff` for scoping and `WebFetch` for spec/docs grounding).
    
    ## References
    
    Load these only when needed:
    
    - [Event-loop ordering trace patterns](references/event-loop-tracing.md) — use when tracing the actual execution order of a specific microtask/macrotask/await sequence in review.
    - [Unhandled-rejection audit checklist](references/rejection-audit.md) — use when systematically auditing a file/module's Promise chains for missing `.catch`/try-catch coverage.
    - [Race-condition and cancellation patterns](references/race-condition-patterns.md) — use when reviewing rapid-repeated-async UI code for AbortController, generation-counter, or equivalent sequencing needs.
    
    ## Response minimum
    
    Return, at minimum:
    
    - the event-loop/ordering verdict for each async chain reviewed, with the traced resolution order (not assumed),
    - the unhandled-rejection audit result for every Promise chain in scope,
    - the listener/timer/observer cleanup audit result, matching every registration to its cleanup call,
    - race-condition risk flags for any rapid-repeated-call pattern lacking sequencing or cancellation,
    - residual risk notes for anything requiring live or load-tested verification beyond this static trace.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related