The 8 async/await mistakes that keep showing up in code review
Async/await bugs are disproportionately expensive because they often fail silently — a missing await in a permission check returns a truthy Promise and grants access, while a swallowed error leaves no trace. A handful of lint rules and one utility function eliminate the most common failure modes.
Roughly 200 code reviews revealed that over half of all bugs trace back to async/await misuse. The syntax looks simple, but its synchronous appearance tricks developers into serializing independent requests, forgetting await inside conditionals, and nesting try-catch blocks into unreadable pyramids. Each mistake has a concrete fix — from a four-line `to()` utility that replaces try-catch hell, to batching with `Promise.all` for controlled parallelism.
A few ESLint rules catch most of these at write time: `no-await-in-loop`, `require-await`, and `no-floating-promises` flag the worst offenders before they reach production. The article also covers the one case where a redundant `return await` is actually required — inside a try-catch, where removing it lets the rejection escape the handler.
The patterns are ranked by real-world frequency, with a quick-reference table mapping each mistake to its consequence and fix. The underlying lesson is that async/await rewards the same discipline as any other concurrency model: know what depends on what, handle errors where you can degrade, and never let a Promise go unawaited.
Async/await's greatest hazard is its own readability — it makes asynchronous code look synchronous, so developers apply synchronous reasoning and miss concurrency and timing bugs.
The `to()` pattern is effectively Go's error-tuple convention ported to JavaScript; it trades exception-style control flow for explicit error checking at the call site.
A missing `await` in a permission guard is a security bug that no amount of unit testing will catch unless the test specifically inspects whether the Promise resolved.
Browsers already cap concurrent connections per domain at six, so unbounded `Promise.all` on large lists creates a hidden queue in the network layer even before the backend sees the traffic.
The one legitimate use of `return await` — inside try-catch — is a subtlety that even experienced developers get wrong because the mental model of "async functions always wrap return values" breaks down when the catch block needs to intercept the rejection.