The event loop is a queue, not a thread
JavaScript runs on a single thread. There's no parallelism — only the appearance of it, managed by the event loop. Picture a main call stack that runs synchronous code top-to-bottom, and alongside it a queue of callbacks waiting for their turn. When the stack empties, the event loop picks the next item off the queue and runs it. That's the whole model.
Promises sit on top of this. When you await something, you're not blocking the thread — you're suspending the current function and handing control back to the event loop. Whatever was next in line gets to run. Your function resumes only when the awaited promise settles and the stack is clear again.
async function fetchUser(id: string) {
console.log("start");
const user = await getUser(id); // suspends here
console.log("got user:", user.name); // resumes later
}
console.log("before");
fetchUser("42");
console.log("after");The output is before → start → after → got user: Alice. That middle "after" trips people up every time. fetchUser starts, hits the await, suspends, and the outer synchronous code finishes before the async function resumes. Once you've wired in the queue mental model, this stops being a gotcha.
Promises aren't asynchronous by themselves
Here's a subtlety that bites even experienced developers: a Promise that resolves synchronously still defers its continuation. Microtasks — the queue that resolved promises feed into — always run after the current synchronous task but before the next macrotask (timers, I/O callbacks).
Promise.resolve("immediate").then((v) => console.log(v));
console.log("sync");
// logs: sync → immediateThe .then callback is not called inline, even though the promise is already resolved. It's queued as a microtask. This is why mixing await with synchronous code demands a clear mental stack: synchronous code always finishes before any .then or post-await code runs, even when no actual I/O is involved.
Practical upshot: never assume an async function's work is done just because you called it without await. You called it, got a Promise back, and whatever happens inside it will run later.
Error handling and the forgotten await
One of the most common async bugs is a floating promise — calling an async function, not awaiting it, and then assuming errors will surface. They won't, not reliably. Unhandled promise rejections either fail silently or crash the process depending on your runtime and version.
// ❌ rejection vanishes into the void
saveRecord(data);
// ✅ rejection is catchable
await saveRecord(data);
// ✅ or explicitly handled
saveRecord(data).catch((err) => logger.error(err));The rule is simple: every promise either gets awaited or has a .catch attached. If neither is true, you have an implicit fire-and-forget that you probably didn't intend.
For sequential work, chain your awaits. For genuinely independent work, Promise.all is your tool — it runs promises concurrently and resolves once all fulfill, or rejects fast if any one fails.
// sequential — slower, fine when order matters
const a = await fetchA();
const b = await fetchB();
// concurrent — faster when they're independent
const [a, b] = await Promise.all([fetchA(), fetchB()]);The difference isn't syntactic nicety. Sequential awaits add latency: if each call takes 200 ms, you wait 400 ms total. Promise.all cuts that to roughly 200 ms for two independent network calls. Profile before assuming either is the right choice.
Reading async code clearly
Once the mental model clicks — event loop as queue, await as a suspension point, microtasks before macrotasks — you can read async code the same way you read synchronous code: by tracing what runs when. Mark every await as a potential yielding point, note what else could run at that gap, and check whether that matters for your logic.
Other modern JavaScript features — destructuring, optional chaining, nullish coalescing — all compose naturally with this style. But the async model itself is the part worth understanding deeply, because misreading it is the source of bugs that are genuinely hard to reproduce.
