跪拜 Guibai
← All articles
Frontend · JavaScript · Interview

The 8 async/await mistakes that keep showing up in code review

By kyriewen ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
Using `await` inside a `for` loop serializes independent requests; use `Promise.all` with `map` to run them in parallel.
A four-line `to()` utility that returns `[err, data]` tuples eliminates nested try-catch blocks.
Forgetting `await` inside a conditional makes the check always truthy because a Promise object is truthy.
`forEach`, `map`, `filter`, and `reduce` do not wait for the Promises returned by their callbacks.
Catching an error and doing nothing is worse than not catching it at all — it erases the only clue you had.
A redundant `return await` at the end of an async function is unnecessary unless the return sits inside a try-catch.
Firing 1,000 requests through `Promise.all` without batching hits browser connection limits and backend rate limits.
Mixing `.then()` chains with `await` in the same function creates two error-handling models that are hard to reason about together.
Enabling `no-await-in-loop`, `require-await`, `no-floating-promises`, `no-misused-promises`, and `await-thenable` in ESLint catches most of these mistakes at write time.
Conclusions

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.

Concepts & terms
to() utility
A small helper that wraps a Promise and returns a `[error, data]` tuple, letting callers handle errors with an `if (err)` check instead of try-catch blocks.
Promise.allSettled
Like `Promise.all`, but waits for all Promises to settle (fulfilled or rejected) and returns an array of outcome objects, so one failure does not abort the rest.
no-floating-promises
An ESLint rule (TypeScript-specific) that flags Promises that are created but not awaited, returned, or otherwise handled — catching the silent `forgot await` bug.
Concurrency cap (browser)
Browsers limit simultaneous HTTP connections to the same origin (typically 6 in Chrome), so firing hundreds of requests at once queues them at the network layer regardless of `Promise.all`.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗