JavaScript runs your code on a single thread. There is one call stack, and only one thing happens at a time. Yet a page can wait for a network request, respond to a click, and run a timer without freezing. The mechanism that makes this possible is the event loop, and understanding it removes most of the mystery from asynchronous code.
One thread, one stack
When a function is called it goes on the call stack; when it returns it comes off. Because there is only one stack, a long synchronous loop blocks everything: the page cannot repaint or respond until it finishes. The trick is that slow operations such as fetching data are not run on this thread at all. They are handed to the browser, which does the waiting and notifies your code when the result is ready.
The queues and the loop
When an asynchronous operation completes, its callback is placed in a queue rather than run immediately. The event loop has one job: when the call stack is empty, take the next callback from a queue and run it. There are two queues that matter. The task queue holds things like timer callbacks and events. The microtask queue holds promise reactions, and it has priority.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// prints 1, 4, 3, 2
The two synchronous logs run first. Then, before the timer callback, the loop drains the entire microtask queue, so the promise log (3) runs before the timer log (2) even though the timer was scheduled first. This priority is why a chain of promises resolves before the next timer ever fires.
Where async/await fits
An async function is a promise-returning function with nicer syntax. Each await pauses the function and schedules the rest of it as a microtask once the awaited promise settles. The thread is never blocked while it waits; other code runs in the meantime, and the function resumes later.
async function load() {
console.log('start');
const data = await fetch('/api').then((r) => r.json());
console.log('after await'); // runs as a microtask, later
return data;
}
Everything before the first await runs synchronously when the function is called. Everything after it runs later, as a continuation. Seeing await as a scheduling point rather than a true pause is the key to predicting the order of output.
Common traps
A few patterns trip people up. A long synchronous computation still freezes the page no matter how many promises surround it, because it never yields the thread; break heavy work into chunks or move it to a worker. Awaiting promises in a loop one by one serializes them when they could run together with Promise.all. And an unhandled rejection in an async function rejects its returned promise silently unless a caller catches it, so every async call needs a try/catch or a .catch somewhere up the chain.
Sequencing without blocking
A frequent question is how to run several async steps in order without nesting. With async/await the answer is simply to await them one after another, which reads like ordinary sequential code while still yielding the thread between steps. The mistake is awaiting inside a loop when the iterations do not depend on each other, which forces them to run one at a time.
// sequential, each depends on the last
const user = await getUser(id);
const orders = await getOrders(user.id);
// independent work: start together, then await
const [a, b] = await Promise.all([getA(), getB()]);
The rule of thumb is to await in sequence only when a later step genuinely needs an earlier result. Otherwise start the work together and await the group, so the waiting overlaps instead of stacking up.
Hold these three ideas together and asynchronous JavaScript stops being surprising: one thread runs the code, the browser does the waiting, and the event loop feeds completed work back in, microtasks first.
Common questions
Why does a promise callback run before a setTimeout(0) callback?
Promise reactions go on the microtask queue, which the event loop drains completely whenever the call stack empties, before it takes the next task such as a timer callback. So microtasks always run ahead of the next timer.
Does await block the thread?
No. await pauses only the async function and schedules its continuation as a microtask once the awaited promise settles. The single thread is free to run other code while it waits.
Why does my page still freeze even with async code?
A long synchronous computation blocks the single thread regardless of surrounding promises, because it never yields. Break the work into chunks or move it to a Web Worker.
What is the difference between a task and a microtask?
Tasks include timers and events and run one per loop turn. Microtasks include promise reactions and are all drained after the current code and before the next task, giving them higher priority.
