messages sent, never delivered
Async IO Misuse
Floating promises, un-awaited tasks, and unhandled rejections: async work whose ordering and errors vanish. The computer's undeliverable mail.
Fire-and-forget work drops results and failures no one notices are gone.
01in the wild
In the wild
Floating Promises
Promises no one awaits run in undefined order and swallow their own errors.
example.js
// SMELL: floating promise -- errors vanish, order is undefined
items.forEach(async (i) => { await save(i); }); // not awaited
doNext(); // runs before any save() finishes
// RIGHT: await the whole batch, surface failures
const results = await Promise.allSettled(items.map(save));
const failed = results.filter(r => r.status === "rejected");
if (failed.length) throw new AggregateError(failed, "some saves failed");forEach ignores the returned promises, so doNext() races ahead and rejections are swallowed. allSettled waits and reports.
// observed
forEach: doNext() runs early; errors lost allSettled: waits for all; failures collected
example.py
import asyncio
# SMELL: task created but never awaited -> may be GC'd mid-flight
async def main():
asyncio.create_task(save(x)) # warning: never awaited
return
# RIGHT: gather and await, propagate exceptions
async def main():
await asyncio.gather(*(save(i) for i in items))An un-awaited task can be garbage-collected before it finishes. gather() keeps references and re-raises errors.
// observed
create_task only: 'Task was destroyed but it is pending!' gather: all complete or first error raised
Awaiting in a Loop
Awaiting each call in turn serializes I/O that could have run concurrently.
example.js
// SMELL: awaiting in a loop runs requests one at a time
const out = [];
for (const id of ids) {
out.push(await fetchUser(id)); // N round-trips, strictly serial
}
// RIGHT: start them together, await as a batch
const out = await Promise.all(ids.map(fetchUser));Each await blocks the next iteration, so total time is the sum of every request. Promise.all overlaps them.
// observed
serial: ~1.2s for 20 ids Promise.all: ~60ms (bounded by the slowest)
example.py
# SMELL: awaiting inside the loop serializes the I/O
results = []
for uid in ids:
results.append(await fetch_user(uid)) # one at a time
# RIGHT: schedule together, await once
results = await asyncio.gather(*(fetch_user(u) for u in ids))await suspends the coroutine until that one call returns before starting the next. gather lets them run at the same time.
// observed
serial: sum of every request's latency gather: bounded by the slowest single request
02cross-pollination
Where this compounds
Nondeterminism
- Out-of-Order Async Completion × var / let / mut Declarations
- Stale Read From a Lagging Async Replica × var / let / mut Declarations
- Plugin Registration / Dispatch-Order Divergence × Macro & Mixin Misuse
Data Corruption
- Unframed Stream Corruption × Unconstrained Inputs
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.