Stop Writing Serial Awaits: A Reality Check
4 mins read

Stop Writing Serial Awaits: A Reality Check

Actually, I should clarify — I rejected a Pull Request last Tuesday that made me want to scream into a pillow. It wasn’t because the code was messy or the variable names were bad. But it was because the developer — who is actually quite good — had turned a 200ms operation into a 4-second ordeal.

They fell for the classic trap. The one that makes async/await so dangerous.

It looks synchronous. It reads like a book. Top to bottom. Line by line. And that is exactly why it kills performance if you aren’t paying attention.

We’ve had async/await in JavaScript for nearly a decade now. And you’d think by 2026 we’d have ironed out the kinks, but I still see the same patterns repeating in codebases running modern Node.js 24.2.0 environments. The syntax is so clean that it hides the fact that you are literally pausing your program.

The Waterfall of Death

Here is the scenario. You need to fetch data for three different user widgets. The dashboard loads, and you need the profile, the notifications, and the subscription status.

JavaScript programming code - JavaScript in Visual Studio Code
JavaScript programming code – JavaScript in Visual Studio Code

The code I reviewed looked something like this:

async function loadDashboard(userId) {
    // ❌ This is the bottleneck
    const profile = await getUserProfile(userId);
    const notifications = await getNotifications(userId);
    const subscription = await getSubscription(userId);

    return { profile, notifications, subscription };
}

But the keyword here is Then.

The Fix: Let Them Race

Unless the second request depends on data from the first (like needing a subscriptionId from the profile to fetch the subscription), there is zero reason to wait.

Fire them all off at once. Await the results together.

async function loadDashboardFast(userId) {
    // ✅ Fire requests immediately
    const profilePromise = getUserProfile(userId);
    const notificationsPromise = getNotifications(userId);
    const subscriptionPromise = getSubscription(userId);

    // Wait for all of them to finish
    const [profile, notifications, subscription] = await Promise.all([
        profilePromise,
        notificationsPromise,
        subscriptionPromise
    ]);

    return { profile, notifications, subscription };
}

The forEach Trap

You have an array of items. You want to save them to the database. You reach for forEach.

JavaScript programming code - Javascript program code programming script background | Premium Vector
JavaScript programming code – Javascript program code programming script background | Premium Vector
async function saveAll(items) {
    // ❌ This does NOT wait!
    items.forEach(async (item) => {
        await db.save(item);
    });
    
    console.log('Done!'); 
}

The modern fix (and yes, this has been the fix for years, but people still ignore it) is for...of loops if you need sequence, or map + Promise.all if you want parallelism. As outlined in my article on Mastering Modern JavaScript Tools.

async function saveAllCorrectly(items) {
    // Option A: Sequence (Slow but safe for order)
    for (const item of items) {
        await db.save(item);
    }

    // Option B: Parallel (Fast)
    await Promise.all(items.map(item => db.save(item)));
}

When Promise.all isn’t enough

I’ve started enforcing usage of Promise.allSettled for these kinds of dashboard aggregations. It’s slightly more verbose to handle the results, but it’s bulletproof.

const results = await Promise.allSettled([
    getUserProfile(userId),
    getNotifications(userId)
]);

const profile = results[0].status === 'fulfilled' ? results[0].value : null;
const notifications = results[1].status === 'fulfilled' ? results[1].value : [];

if (!profile) {
    throw new Error("Critical failure: Profile missing");
}
// If notifications failed, we just show an empty list. Graceful degradation.

A Note on Top-Level Await

Node.js logo - Node.js Logo PNG Vector (SVG) Free Download
Node.js logo – Node.js Logo PNG Vector (SVG) Free Download

But be careful using it in your main application entry points. It turns your module loading into an async operation. If you have a chain of imports that all use top-level await, you are essentially recreating the serial waterfall problem I mentioned earlier, but at the module loader level. Your app startup time can creep up silently, as discussed in my article on The ES Modules Hangover.

The Reality

Async/await is syntactic sugar. Sweet, delicious sugar. But it doesn’t change how the event loop works. It doesn’t magically make JavaScript multi-threaded (Worker Threads do that, but that’s a whole other article). It just hides the callback hell.

Leave a Reply

Your email address will not be published. Required fields are marked *