Understanding async/await pitfalls in JavaScript
Async/await makes asynchronous code look synchronous. But that simplicity hides pitfalls that can cause bugs, memory leaks, and performance issues.
Pitfall 1: Sequential When You Mean Parallel
// Slow: 2 seconds (sequential)
const users = await getUsers();
const orders = await getOrders();
// Fast: 1 second (parallel)
const [users, orders] = await Promise.all([
getUsers(),
getOrders()
]);
```
If two operations don't depend on each other, run them in parallel.
## Pitfall 2: Unhandled Rejections
```javascript
// Bad: if getUser throws, the error silently disappears
async function loadDashboard() {
getUser(123).then(u => updateUI(u)); // No await, no catch
}
// Good: handle the error
async function loadDashboard() {
try {
const user = await getUser(123);
updateUI(user);
} catch (err) {
showError(err);
}
}
```
In Node.js, unhandled rejections crash the process (as of Node 15+).
## Pitfall 3: Async in forEach
```javascript
// BROKEN: forEach doesn't await
const ids = [1, 2, 3];
ids.forEach(async (id) => {
await processItem(id); // These run in parallel, not sequential
});
console.log('Done!'); // Runs before processing finishes
// Fix: for...of for sequential
for (const id of ids) {
await processItem(id);
}
// Fix: Promise.all for parallel
await Promise.all(ids.map(id => processItem(id)));
```
## Pitfall 4: Async Constructor Trap
```javascript
// Can't use async constructor
class Database {
constructor() {
// This doesn't work as expected
this.connection = await connect(); // SyntaxError
}
}
// Fix: factory function
class Database {
static async create() {
const db = new Database();
db.connection = await connect();
return db;
}
}
const db = await Database.create();
```
## Pitfall 5: Error Swallowing in Promise.all
```javascript
// If one fails, all results are lost
try {
const results = await Promise.all([
fetchUser(1), // succeeds
fetchUser(999), // fails
fetchUser(2), // succeeds but result lost
]);
} catch (err) {
// Only get the first error, lose all successful results
}
// Fix: Promise.allSettled
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(999),
fetchUser(2),
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`User ${i}: ${result.value.name}`);
} else {
console.log(`User ${i} failed: ${result.reason}`);
}
});
```
## Pitfall 6: Awaiting Non-Promises
```javascript
// Works but unnecessary overhead
const x = await 42; // Wraps in Promise.resolve(42)
const y = await someObj; // Wraps non-thenable
// This matters in loops
for (const item of items) {
const result = await syncFunction(item); // Don't await sync functions
}
```
## Pitfall 7: Missing Return in Async
```javascript
// Subtle bug: returns undefined, not the user
async function getUser(id) {
const user = await db.findUser(id);
if (!user) throw new NotFoundError();
user; // Oops, forgot 'return'
}
```
## The async/await Checklist
1. **Independent operations?** → `Promise.all`
2. **Need all results even if some fail?** → `Promise.allSettled`
3. **Iterating async?** → `for...of`, never `forEach`
4. **Error handling?** → `try/catch` at boundaries
5. **Cleanup needed?** → `try/finally`
---
What async bug have you spent the most time debugging?
All rights reserved