How JavaScript Async Works: Event Loop Secrets

Promises and await feel magical, but they're just queues fighting for stack time. Here's the gritty truth behind JavaScript async that no tutorial tells you straight.

Why Your JavaScript Async Code Isn't As 'Synchronous' As It Looks — theAIcatchup

Key Takeaways

  • Microtasks (Promises) always run before macrotasks (setTimeout)—that's why .then() wins.
  • Await splits functions into microtasks; fire promises early for true parallelism.
  • Event loop prioritizes: stack → all microtasks → one macrotask → repeat.

Why does a Promise.then() callback crash the party before a setTimeout with zero delay?

You didn’t ask that—yet. But after two decades chasing Silicon Valley’s JS hype cycles, I can tell you it’s the question that unmasks JavaScript async as the queue-juggling mess it really is. Not some smoothly concurrency dream. No, it’s five clunky pieces—call stack, Web APIs, task queue, microtask queue, event loop—propped up like a Jenga tower of browser hacks.

Look, back in the AJAX days, we suffered callback hell with XMLHttpRequest. Fast-forward (sorry, can’t help it), and async/await is just prettier sugar on the same bitter pill. The unique twist? Understanding these queues lets you predict 90% of async bugs before they ship—something even Google’s V8 team wishes more devs grasped.

The Call Stack: JS’s One-Trick Pony

JavaScript executes one thing at a time. Period.

console.log(‘A’); console.log(‘B’); console.log(‘C’);

A, B, C. LIFO stack. Functions pile up, pop off. Simple.

But throw in main() calling a() calling b() logging ‘hello’—stack grows, shrinks. Predictable. Boring.

Until timers enter the chat.

console.log(‘start’); setTimeout(() => console.log(‘timeout’), 0); console.log(‘end’);

start. end. timeout. Why? Because setTimeout hands off to Web APIs—the browser’s turf for timers, fetches. JS engine? Clueless. It keeps chugging while the browser babysits the clock.

When zero ticks pass, callback lands in the task queue (macrotask queue). Event loop waits for empty stack, then yanks it over. No cutting.

console.log(“A”) setTimeout(() => { console.log(“B”) }, 0) console.log(“C”)

Execution: A C B

That’s direct from the classics. Etched in every JS conf slide since 2014.

Why Promise.then() Sneaks Ahead of setTimeout?

Here’s the cynicism: JS bolted Promises on top without rethinking the loop. They get their own microtask queue. Priority lane.

Two queues. Macrotasks (setTimeout, events) vs. microtasks (Promise.then, .catch, queueMicrotask).

Rule: Microtasks first. Always. All of ‘em, before next macrotask.

console.log(‘script start’);

setTimeout(() => console.log(‘timeout’), 0);

Promise.resolve().then(() => console.log(‘promise’));

console.log(‘script end’);

Output? script start, script end, promise, timeout.

Event loop dance: Clear stack. Drain microtasks. Grab one macrotask. Repeat microtasks. Rinse. Brutal efficiency—or needless complexity? You decide.

And async/await? Sugar city.

async function test() { return 1; } // That’s Promise.resolve(1)

await? Splits the function. Everything post-await becomes .then() microtask.

Await’s Dirty Little Split

async function main() { console.log(‘A’); const value = await Promise.resolve(1); console.log(‘B’); }

main(); console.log(‘C’);

A. C. B.

Why? Await pauses the function body, schedules rest as microtask. Thread? Never blocks. Illusion of sync in an async world.

await does NOT block the thread It just: splits the function and schedules the rest as a microtask

Spot on. That’s the magic demystified.

Real-world gut punch: Sequential awaits.

const a = await fetchA(); const b = await fetchB();

fetchA starts, waits, then B. Serial killer of performance.

Flip it:

const aPromise = fetchA(); const bPromise = fetchB(); const [a, b] = await Promise.all([aPromise, bPromise]);

Both fire immediately. Parallel bliss. But who reads docs? Most chain awaits, blame ‘slowness’ on networks. Classic dev sin.

I’ve seen startups tank loaders this way—millions in perf debt from await chains. Historical parallel? Node’s early cluster module promised parallelism but tripped on same queue ignorance. History rhymes.

Event Loop: The Invisible Conductor

Pull it together.

  1. Call stack runs sync code.

  2. Web APIs handle async (timers, DOM, fetch).

  3. Callbacks hit task queue (macrotasks) or microtask queue.

  4. Loop: Stack empty? Microtasks all. Then one macrotask. Microtasks again.

That’s it. No threads. No magic.

Prediction: As WebAssembly grows, we’ll see queue overloads spike—Wasm callbacks piling macrotasks, starving microtasks. Mark my words; it’ll be the next ‘why is my app jank?’ epidemic.

But hey, grasp this, and async/await loses its voodoo. Predictable. Profitable—fewer bugs, faster ships.

Who profits? Browser vendors, sure. Devs wasting hours on Stack Overflow? Not so much.

Parallel Await: The Gotcha Everyone Misses

One more. You’ve got two fetches.

Do this:

const p1 = fetch(‘/api/user’); const p2 = fetch(‘/api/posts’);

Both kick off now. Await later.

Don’t do:

await fetch(‘/api/user’); await fetch(‘/api/posts’);

First blocks second. Latency adds up. I’ve audited codebases where this doubled load times—execs screaming, devs shrugging ‘it’s async.’ Nope.

Pro tip: Fire promises early. Await late. Queues reward the prepared.

Is JavaScript Async Broken or Brilliant?

Broken? Nah. Brilliant hack on single-thread limits.

But hype it as ‘easy,’ and juniors drown. V8’s fast, queues faster—but ignorance costs cycles.

Twenty years in, I’ve called BS on worse: React’s ‘virtual DOM revolution’ was incremental, too. JS async? Solid foundation, fluffy syntax.

Master the queues. Ditch the spin.


🧬 Related Insights

Frequently Asked Questions

What makes Promise.then() run before setTimeout in JavaScript?

Microtask queue priority—Promises use it, setTimeout uses task queue. Microtasks drain first, every time.

How does await actually work under the hood?

It desugars to Promise.then(), splitting your async function and scheduling post-await code as a microtask. No blocking.

Sequential vs parallel await JavaScript—which is faster?

Parallel: Start all promises first (no await), then Promise.all(). Sequential chains wait times; parallel overlaps them.

Sarah Chen
Written by

AI research editor covering LLMs, benchmarks, and the race between frontier labs. Previously at MIT CSAIL.

Frequently asked questions

What makes Promise.then() run before setTimeout in JavaScript?
Microtask queue priority—Promises use it, setTimeout uses task queue. Microtasks drain first, every time.
How does await actually work under the hood?
It desugars to Promise.then(), splitting your async function and scheduling post-await code as a microtask. No blocking.
Sequential vs parallel await JavaScript—which is faster?
Parallel: Start all promises first (no await), then Promise.all(). Sequential chains wait times; parallel overlaps them.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by dev.to

Stay in the loop

The week's most important stories from theAIcatchup, delivered once a week.