---
name: promises-async-await
description: "Audits and fixes promise and async/await code in JavaScript and TypeScript: floating promises, forEach(async), accidental sequential awaits, the wrong Promise.all, allSettled, any or race, swallowed errors, unhandled rejections, async timers and handlers. Runs a scanner, then edits flagged lines only. Use for 'await in forEach', 'unhandled promise rejection', 'Promise.all fails', 'runs in the wrong order' or an async code review. Do NOT use for cancellation or timeouts, event loop theory, or TypeScript typing."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/javascript/promises-async-await/
  category: frontend-development
  subcategory: javascript
  version: "1.0.0"
  created: "2026-11-05"
  updated: "2026-11-05"
---

# Promises and Async/Await: Find and Fix the Hazards

An agent works on async code in this order: decide for each call site whether it must run in sequence, may run in parallel or is meant to be fire-and-forget, run the scanner, choose the right combinator for every group of promises, put error handling where something can act on it, make every rejection reach a handler, then re-run the scanner and the project's tests. The scanner reads text. It cannot know whether a function returns a Promise, so it reports hazards for a person or agent to confirm.

Reusable code is in `references/patterns.md`. Lint rules, runtime flags and the rival comparison are in `references/appendix.md`.

## When to use this skill, and when not to

Use it when async code runs in the wrong order, a loop finishes before its work does, a rejection kills a Node process or logs as unhandled, a batch fails or half-fails silently, or a review asks for async hygiene.

| Request | Use instead |
|---|---|
| Timeouts, cancelling fetches, stale responses, abort listeners | `abortcontroller-cancellation.md` |
| Typing `Promise<T>`, generics, `satisfies` | A TypeScript skill. Out of scope |
| How the event loop and microtasks work, in depth | Read the HTML standard and Jake Archibald's article. Out of scope |
| Data fetching libraries, caching, retries at app level | A state and data fetching skill. Out of scope |
| Worker threads and real parallelism | Out of scope. Promises give concurrency on one thread |

## Guardrails

1. **Propose, then change.** Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Behaviour changes are not refactors.** Turning a sequential loop into `Promise.all` changes request order, load and failure behaviour. Say so in the report and get approval per file.
3. **Never run the user's code to test a fix against live services.** Writes, payments, emails and deletes must not be replayed. Use the project's own tests, mocks or a test environment.
4. **Data handled.** The scanner reads source files and prints file, line, code and a fixed message. It prints no source text, so error messages and request bodies in the code are not copied into the report. Do not paste tokens, API keys, customer data or `.env` values into prompts or reports.
5. **Nothing is invented.** Every rule carries a source and a date. Version-dependent behaviour carries **verify before relying on this**.
6. **Refuses to:** add `.catch(() => {})` to silence a warning, swallow an error to make a test pass, use `void` as a fix without a handler on the promise, disable a lint rule file-wide, or declare a fix verified without re-running the scanner and the tests.

## Inputs the agent must have before starting

Ask for every missing item. Do not assume.

| Input | Ask exactly |
|---|---|
| Scope | "Which files or folders should I review? Give paths, not 'the app'." |
| Runtime | "Is this Node, a browser, or both? Which Node major version and which browsers?" |
| Symptom | "What goes wrong: wrong order, early finish, crash, silent failure, slowness?" |
| Limits | "Does any API called here have a rate limit or a concurrent request limit? What is it?" |
| Tests | "What command runs the tests, and is it safe to run on this machine?" |
| Lint | "Is ESLint with type information configured? If not, may I run only the scanner?" |
| Approval | "May I edit on a branch in batches and show each diff first?" |

## Workflow

### Step 1: Classify each call site

List every group of async calls in scope. Give each one intent before touching it.

| Intent | Signs | Target shape |
|---|---|---|
| Sequential | Later call needs the earlier result; order of side effects matters; payments, writes, rate-limited APIs | `for...of` with `await` |
| Independent | No call uses another's result | `await Promise.all(...)` or `allSettled`, within the limit in step 4 |
| Fire-and-forget | Result not needed, failure must not break the caller | Call with a handler: `.catch(log)` |
| Redundant sources | Same data from several sources, first good answer wins | `Promise.any` |

If intent cannot be read from the code, ask the user. Do not guess for writes, payments or emails.

### Step 2: Run the scanner

```
python3 scripts/async_check.py src | head -60
```

Read only its output. Do not read the script. The first line has the counts. `--strict` exits 1 on warnings as well as errors. It skips `node_modules`, `dist`, `build` and `.git`.

| Code | Level | Meaning | Fix |
|---|---|---|---|
| `foreach-async` | error | `.forEach(async ...)`: every callback starts, nothing waits | Step 4 |
| `executor-async` | error | `new Promise(async ...)`: a throw inside is lost | Step 5 |
| `floating-then` | warn | `.then()` statement with no `.catch` in the same statement | Step 6 |
| `map-async-statement` | warn | `.map(async ...)` result dropped | Step 3 |
| `all-not-awaited` | warn | `Promise.all(...)` result dropped | Step 3 |
| `race-timeout` | warn | `Promise.race` with a timer: loser keeps running, timer never cleared | `abortcontroller-cancellation.md` |
| `timer-async` | warn | async callback in `setInterval` or `setTimeout` with no `try` nearby | Steps 5 and 6 |
| `empty-catch` | warn | Catch that does nothing | Step 5 |
| `await-in-loop` | info | `await` in a loop body | Step 4 |
| `return-in-try` | info | `return call()` inside `try` | Step 5 |
| `explicit-construction` | info | `new Promise` wrapping a Promise | Step 5 |

The scanner splits on lines and tracks braces, not syntax. A hit inside a string, template or comment is a false positive: read the line. It does not find a missing `await`. For that, use type-aware linting (`references/appendix.md`).

### Step 3: Choose the combinator

Source: MDN, Promise, fetched 5 November 2026.

| Combinator | Fulfils | Rejects | Empty array |
|---|---|---|---|
| `Promise.all` | Array of values when all fulfil | First rejection, at once | Fulfils with `[]` |
| `Promise.allSettled` | Array of `{status, value\|reason}` when all settle | Never | Fulfils with `[]` |
| `Promise.any` | First fulfilled value | `AggregateError` only if all reject | Rejects with `AggregateError` |
| `Promise.race` | Mirrors the first promise to settle | Same | Pending forever |

| Situation | Use |
|---|---|
| Every result is needed and one failure makes the batch useless | `all` |
| Partial results are useful (bulk send, bulk lookup) | `allSettled`, then handle every entry with `status === "rejected"` (step 6) |
| Redundant mirrors or fallbacks | `any` |
| First answer wins and the others can be abandoned | `race` only with a signal that stops the losers (`abortcontroller-cancellation.md`) |

Three rules the MDN page states: `all` and `race` do not cancel the other inputs, their handlers stay attached; a long-pending promise reused in a loop collects handlers that are never removed; a promise starts work when it is created, not when it is awaited. Guard `race` with `if (!items.length)`.

When the scanner reports `map-async-statement` or `all-not-awaited`, wrap the expression in `await Promise.all(...)` or `await Promise.allSettled(...)` according to the table.

### Step 4: Sequential or parallel, and how many at once

| Case | Change |
|---|---|
| `forEach(async ...)` and order matters | `for (const x of xs) { await ... }` |
| `forEach(async ...)` and items are independent | `await Promise.all(xs.map(async (x) => ...))` or `allSettled` |
| `await-in-loop` and items are independent | Parallelise, subject to the limit below |
| `await-in-loop` and an API has a rate limit, or a write must follow a write | Leave sequential. Add a comment saying why |
| Independent items, 10 or fewer, no documented limit | `Promise.all` directly |
| Independent items, more than 10, or a documented limit | Limiter in `references/patterns.md`, section 1 |

Set the limiter's concurrency to the lower of the API's documented concurrent limit and 5. 5 is a starting value, not a measured optimum: tune it against the API and the machine, and say so in the report. Every parallelised loop is a behaviour change (guardrail 2).

A V8 engineering post (12 November 2018) records that `await` on a native promise was cut from three microtask ticks to one in V8 7.2. Do not remove an `await` or add a promise chain for speed.

### Step 5: Put error handling where it can act

| Pattern | Problem | Fix |
|---|---|---|
| `try { return fn(); } catch {}` | A rejection from `fn()` skips the catch | `return await fn()` inside `try` |
| `.catch((e) => { log(e); })` then more `.then` | The chain recovers and carries on with `undefined` | Rethrow if callers must know (MDN) |
| `catch (e) { return null; }` | Error turned into data: the caller cannot tell failure from no result | Rethrow, or return a typed result the caller must check |
| Empty catch | Nothing logged | Log with context and rethrow, or comment why ignoring is safe |
| `new Promise(async (res) => ...)` | A throw inside the async executor is not routed to `reject` | Write an async function and return it |
| `throw` inside a `setTimeout` inside an executor | Not caught by the promise (javascript.info) | Call `reject` from the timer callback |
| `new Promise` around an API that already returns a Promise | Extra layer, easy to lose errors | Return the inner promise |

Place a catch only where code can do something: show a message, retry, fall back, add context and rethrow. A single catch at the top of a request handler is usually enough. `Promise.try()` wraps a function that may throw synchronously or return a promise: MDN does not give its Baseline status in the text read on 5 November 2026, so **verify before relying on this** and prefer `async () => fn()` where support is unknown.

### Step 6: Make every rejection reach a handler

Sources: Node.js, process documentation; typescript-eslint, `no-floating-promises`; javascript.info, error handling with promises. All fetched 5 November 2026.

| Fact | Consequence |
|---|---|
| Node emits `unhandledRejection` when a rejected promise has no handler within a turn of the event loop | A late `.catch` is too late to prevent the event. `rejectionHandled` fires afterwards |
| With no listener, Node raises it as an uncaught exception: stack trace to stderr, exit code 1 | A missing catch can crash a server. The page names `throw` as the default; **verify before relying on this** for the exact version that made it the default |
| Browsers fire `unhandledrejection` on `window` with `promise` and `reason` | Add one global listener that reports to your error tracker |
| `void somePromise()` marks it intentional, but does not handle it | Rejections still go unhandled |
| `.finally()` without `await` is still floating | Chain `.catch` after it |
| An array of promises not passed to `all`, `allSettled`, `any` or `race` is floating | Combine it |

Rules for each case the scanner reports:

1. `floating-then`: add `.catch(handler)` where the failure can be handled, or `await` it, or `return` it.
2. Fire-and-forget: call it as `task().catch((e) => log("task failed", e))`. The handler must log, not stay empty.
3. `allSettled`: after it resolves, filter `status === "rejected"`, log each `reason` with the item it belongs to, and return or throw a summary. Never ignore the array.
4. `timer-async`: put `try/catch` inside the callback. For `setInterval`, add a `running` flag so a slow run cannot overlap the next one.
5. A global `unhandledrejection` or `process.on("unhandledRejection")` listener is for reporting. It is not a replacement for handlers, and Node's page warns that code after an uncaught exception is not safe to resume.

### Step 7: Verify and report

1. Re-run the scanner. Errors must be 0. Each warning is fixed or listed with a reason.
2. Run the project's tests with the command the user gave. Report pass and fail counts, not output.
3. If type-aware linting exists, run `no-floating-promises` and `no-misused-promises` and report the counts (`references/appendix.md`).
4. For each parallelised loop, state the old and new concurrency and what the user must confirm against API limits.
5. State what is untested: behaviour against live services, ordering under real latency, load.
6. Use the report template. Do not echo unchanged code back.

## Worked example

An invented file, `orders.js`, 39 lines: a `sendAll` that uses `forEach(async ...)` and logs "all receipts sent" straight away, a `loadTotals` loop that awaits one order at a time, a `warm` function with a floating `.then` and a `Promise.race` against a timer, a `safeGet` that returns a promise inside `try`, a `setInterval(async ...)` with no handler and a `.catch(() => {})`.

```
$ python3 scripts/async_check.py orders.js
1 errors, 4 warnings, 2 info, 1 files
orders.js:4 error foreach-async        .forEach(async ...) starts every callback and waits for none. ...
orders.js:14 info  await-in-loop        await inside a loop runs one at a time. ...
orders.js:21 warn  floating-then        .then() with no .catch(): a rejection is unhandled. ...
orders.js:22 warn  race-timeout         Promise.race with a timer: the loser keeps running ...
orders.js:27 info  return-in-try        return of a call inside try: if it returns a Promise ...
orders.js:33 warn  timer-async          async timer callback: rejection is unhandled ...
orders.js:38 warn  empty-catch          empty catch swallows the error. ...
```

(Messages shortened here.) The agent classifies `sendAll` as independent with partial results useful, so it uses `allSettled` and reports failures. `loadTotals` is independent and needs every result, so it uses `Promise.all`. `warm` gets a `.catch` on the audit call, and the race is replaced by passing `AbortSignal.timeout(3000)` to the operation, which is the job of `abortcontroller-cancellation.md`. `safeGet` gains `await` and rethrows after logging. The interval gets a `try/catch/finally` and a `running` flag. `swallow` loses its empty catch.

```
$ python3 scripts/async_check.py orders.js
0 errors, 0 warnings, 0 info, 1 files
```

The report names two behaviour changes: `sendAll` now sends all receipts at once (the agent cannot see how many ids callers pass, so it asks the user for the real count and any mail API limit) and `loadTotals` now issues its lookups together. The scanner passing does not show that either change is safe for the API. The user decides.

## Templates

### Report

```
Async report: <repo or folder>, <date>
Files: <count>. Runtime: <Node version / browsers>. Tests: <command>.
async_check before: N errors, N warnings, N info. After: N errors, N warnings, N info.
Type-aware lint (if run): no-floating-promises N -> N, no-misused-promises N -> N.
Changes: <file:line  code  old -> new>   (one line each)
Behaviour changes needing approval: <file  old concurrency -> new  limit to confirm>
Left as is: <file:line  reason>
Untested: live services, real latency, load (edit to match)
```

### Starting prompt

```
Use the promises-async-await skill. Scope: <paths>. Runtime: <Node N / browsers>.
Symptom: <wrong order | early finish | crash | silent failure | slow>.
API limits: <none | N concurrent, N per second>. Tests: <command>.
Edit on a branch in batches of one file and show each diff first.
```

### Partial-failure handler

```js
const results = await Promise.allSettled(items.map((item) => work(item)));
const failures = results
  .map((r, i) => (r.status === "rejected" ? { item: items[i].id, reason: r.reason } : null))
  .filter(Boolean);
if (failures.length) logger.error("batch partly failed", { failed: failures.length, of: items.length });
return { ok: results.length - failures.length, failures };
```

Log item ids, not item contents: items can hold names, emails or card references.

## Failure modes and anti-patterns

| Mistake | How to spot it | Do instead |
|---|---|---|
| `forEach(async ...)` | `foreach-async`; code after the loop runs first | `for...of` or `Promise.all` with `map` (step 4) |
| `allSettled` with the results ignored or only the fulfilled ones used | The result array is never read for `rejected` | Handle every rejected entry (step 6) |
| Expecting `Promise.all` or `race` to stop the other work | Work continues after the batch rejects | Pass a signal to the work (`abortcontroller-cancellation.md`) |
| Awaiting independent calls one by one | `await-in-loop` and a slow total time | Parallelise within the limit (step 4) |
| A catch that logs and returns `null` | Callers treat missing data as success | Rethrow or return a checked result (step 5) |

## Quality bar

Run every item before handing over.

1. `async_check.py` errors are 0, and every warning and info line is fixed or listed with a reason.
2. No `.forEach(async ...)` remains.
3. Every `allSettled` result is inspected for `rejected` entries.
4. Every fire-and-forget call has a handler that logs.
5. Every parallelised loop states its old and new concurrency, and the limiter value is labelled a starting value.
6. No catch block is empty, and none returns a value that hides the failure.
7. The project's tests were run with the user's command, or the report says they were not.
8. The report lists what was not tested and which changes alter behaviour.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the visible text of pages I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 99 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 5,045 | Estimate | When the skill fires |
| C. `references/patterns.md` | 1,034 | Estimate | Step 4 for the limiter, step 6 for the fire-and-forget helper and global reporting, or retry and `withResolvers` code |
| C. `references/appendix.md` | 1,006 | Estimate | Only for lint set-up, runtime flags, availability notes or the rival comparison |
| Script output (typical run) | 340 | Estimate | Scanner output measured on the worked example (1,184 characters) |
| D. Typical run (A + B + script output) | 5,484 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 8,184 | Estimate | Large input, every branch |
| E. Cheaper path | 5,484 | Estimate | Run the scanner, fix only flagged lines, skip both references and type-aware lint |

The three rivals I read have bodies of about 270 to 880 tokens (estimates from visible text). None ships a scanner, and none tells the agent how to choose between `all`, `allSettled`, `any` and `race`. This skill costs more to load, and in return the scanner finds the hazards so the agent does not read every file to look for them.

## Handoffs

`abortcontroller-cancellation.md` covers timeouts, cancelling work and abort listeners. Both skills read the same MDN and DOM standard pages, so neither repeats the other.

## Appendix

Reusable code is in `references/patterns.md`. Lint rules, runtime flags, availability notes and the rival comparison are in `references/appendix.md`. Read them only when a step points there.

## Sources

Last verified 5 November 2026 for every source.

- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- MDN, Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- Node.js, process, `unhandledRejection` and `rejectionHandled`: https://nodejs.org/api/process.html#event-unhandledrejection
- typescript-eslint, `no-floating-promises`: https://typescript-eslint.io/rules/no-floating-promises/
- javascript.info, Error handling with promises: https://javascript.info/promise-error-handling
- Jake Archibald, Tasks, microtasks, queues and schedules: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- V8 blog, Faster async functions and promises (12 November 2018): https://v8.dev/blog/fast-async
- Emily Scott, Why JavaScript forEach does not work with await (DEV Community): https://dev.to/idioms/why-javascript-foreach-does-not-work-with-await-how-to-fix-it-3idj
- jeffallan, javascript-pro. Rival implementation: https://skills.browseract.com/skills/jeffallan_claude-skills_javascript-pro
- pluginagentmarketplace, asynchronous. Rival implementation: https://skills.cat/skills/pluginagentmarketplace/custom-plugin-javascript/asynchronous
- wshobson, modern-javascript-patterns. Rival implementation: https://agent-skills.md/skills/wshobson/agents/modern-javascript-patterns

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/javascript/promises-async-await/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
