← all posts

async/await made me a worse programmer (briefly)

·2 min read ·JavaScript · Programming · Async

When async/await arrived in JavaScript, it felt like a superpower. Asynchronous code that read like synchronous code. No more callback pyramids, no more .then().then().then() chains. I used it everywhere immediately.

And for about six months, it quietly made me worse at handling asynchronous code.

What async/await Hides

await pauses execution until the Promise resolves. It looks synchronous. It's not. And when you treat it as truly synchronous, you write code like this:

// Sequential — takes 3 seconds
const user = await getUser(id);         // 1 second
const posts = await getPosts(id);       // 1 second
const comments = await getComments(id); // 1 second

Those three requests are independent. They don't need to wait for each other. But because await looks like =, it's easy to miss that you're running them in sequence.

// Concurrent — takes 1 second
const [user, posts, comments] = await Promise.all([
    getUser(id),
    getPosts(id),
    getComments(id),
]);

The Error Handling Trap

async function fetchData() {
    const data = await riskyOperation(); // Throws — unhandled!
    return data;
}

An unhandled rejected Promise in an async function doesn't crash synchronously where you'd notice it. It becomes an unhandled promise rejection. Use try/catch, or await in a context that handles rejections:

async function fetchData() {
    try {
        return await riskyOperation();
    } catch (error) {
        logger.error('fetchData failed', { error });
        throw error; // Re-throw if the caller needs to handle it
    }
}

The Lesson

async/await is excellent syntax. But it's a syntax improvement over Promises, not a replacement for understanding them. Know what's happening underneath. Use Promise.all for concurrent operations. Handle rejections explicitly. Don't let the synchronous appearance create synchronous thinking.