Explanation
What just happened
A ticker logged every 20 ms. Then a timer callback started a 200 ms busy loop. The next tick was due, but its callback runs on the same thread as the loop, so it had to wait. No tick printed until slow work ends.
Try the what-if switch
- Worker: the busy loop runs in a
Worker, a real second thread with its own event loop. The main thread stays free and keeps ticking the whole time.
Why
The event loop can only take the next callback when the current one returns. A long synchronous function never returns early, so everything queued behind it waits, like timers and promise callbacks.
Watch out
A Worker doesn’t share your variables. postMessage copies the data, so a change made in the worker doesn’t show up in the main thread unless you send it back.
Common questions
Is JavaScript single-threaded?
Your JavaScript runs on one thread per realm: the main thread, plus one per Worker. While a function runs, no other callback, timer or promise on that thread can run.
Does async make CPU-heavy code non-blocking?
No. An async function runs synchronously until its first await. A busy loop inside it blocks the thread just the same.
How do I run CPU-heavy work without blocking?
Move it to a Worker (node:worker_threads in Node, Web Workers in browsers). It runs on its own thread with its own event loop, and talks to the main thread through postMessage, which copies the data.
Verified: this timeline matches a real program, checked in CI on Node.js 22 and 24. How we verify. Unclear or wrong? Tell us