Async/Await is Misleading
Updated February 21, 2026
Async/await exists to make asynchronous code read like synchronous code. That's the sales pitch, and it's true - as far as it goes. A single async operation does read more cleanly with await than with nested callbacks or chained .then() calls.
But there's a trade-off hiding behind that cleaner syntax, and most developers never think about it explicitly: async/await makes the sequential flow of one operation easier to read while making the concurrent state of your whole program harder to reason about.
That's a real trade-off, and in most programs, it's the wrong one.
The Problem: Invisible Interleaving
Your program isn't one operation. It's many operations happening concurrently, often sharing state. Every await is a point where your function yields control. Other code runs. State changes. When execution resumes after the await, the world may look different than it did before.
The syntax hides this completely. Consider:
let balance = 0;
async function deposit(amount) {
const current = balance;
await sendConfirmationEmail(amount);
balance = current + amount;
}
Read it top to bottom and the intent is clear. Grab the current balance, send a confirmation, update the balance. Now imagine two deposits fire concurrently - deposit(50) and deposit(30). Both read balance as 0. Both await. Both resume and write their result. One writes 50, the other writes 30. You lost a deposit. The final balance is 30 or 50, never 80.
This is a classic read-modify-write race, and it's happening on a plain local variable. No databases, no network partitions, no distributed systems. Just two async functions sharing in-memory state within a single JavaScript process. The syntax reads like a sequence that owns balance from start to finish. It doesn't.
This isn't a contrived edge case. It's the default behavior. Any in-memory state touched on both sides of an await is vulnerable. Async/await just makes it invisible.
What You're Actually Trading
Without async/await, asynchronous code looks different from synchronous code. Callbacks, promises, event handlers - they're noisier, but that noise carries a signal: this code doesn't execute in a straight line. You're forced to confront the concurrency because the syntax won't let you forget it.
Async/await removes that signal. You get code that reads like a sequential script, but executes with all the same concurrency hazards as before. The hazards didn't go away. You just stopped seeing them.
Here's the trade-off stated plainly:
- What you gain: Individual async operations read more linearly.
- What you lose: Any visual indication that state can change between your statements.
For a script that runs one operation start to finish, that's a fine trade. For a server handling concurrent requests, a UI responding to simultaneous user actions, or anything else with shared mutable state, you've traded a minor readability improvement for a major reasoning deficit.
The State Management Angle
The deeper issue is about state. Correct concurrent programs require you to think carefully about what state exists, who can modify it, and when modifications are visible to other operations. Async/await doesn't help with any of that. Worse, it actively discourages thinking about it by presenting concurrent code in sequential clothing.
Compare two approaches to handling incoming messages:
// Async/await style - looks sequential, hides interleaving
async function handleMessage(msg) {
const state = await getState();
const newState = computeNewState(state, msg);
await setState(newState);
}
// Explicit state machine - state transitions are atomic and visible
function handleMessage(state, msg) {
switch (state.status) {
case "idle":
return msg.type === "start"
? { status: "processing", data: msg.payload }
: state;
case "processing":
return msg.type === "complete"
? { status: "idle", result: msg.result }
: state;
}
}
The first version looks simpler. The second version is simpler - in the ways that matter. State transitions are pure functions. There's no window where state is being read by one operation and written by another. The current state and the valid transitions are right there in the code. You can reason about every possible state your program can be in by reading it.
What Actually Helps
Patterns that make concurrency safe share a common trait: they force you to be explicit about state and its transitions.
Event-driven architectures process one message at a time against a known current state. Actor models isolate state behind message boundaries. State machines make valid transitions enumerable. Functional patterns avoid shared mutable state entirely. These approaches don't make your code look sequential. They make it be safe. That's the right trade-off.
Async/await optimizes for the wrong thing. It optimizes for how the code reads when you're writing a single operation. But the hard problem in concurrent programming was never "how do I write one operation?" It's "how do I make sure all my operations compose correctly?" Async/await is silent on that question, and its syntax actively obscures it.
The trade-off isn't worth it. Write code that tells you what's actually happening.