DEV Community

EME GUG
EME GUG

Posted on

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()
]);
Enter fullscreen mode Exit fullscreen mode

If two operations don't depend on each other, run them in parallel.

Pitfall 2: Unhandled Rejections

// 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

In Node.js, unhandled rejections crash the process (as of Node 15+).

Pitfall 3: Async in forEach

// 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)));
Enter fullscreen mode Exit fullscreen mode

Pitfall 4: Async Constructor Trap

// 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();
Enter fullscreen mode Exit fullscreen mode

Pitfall 5: Error Swallowing in Promise.all

// 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}`);
    }
});
Enter fullscreen mode Exit fullscreen mode

Pitfall 6: Awaiting Non-Promises

// 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
}
Enter fullscreen mode Exit fullscreen mode

Pitfall 7: Missing Return in Async

// 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'
}
Enter fullscreen mode Exit fullscreen mode

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?

Top comments (0)