A Slow Microtask Delays A Timer Too
The last post proved microtasks jump the line with a step-through visualizer. This one proves the sharper, more surprising claim: a slow microtask delays a timer exactly the way slow synchronous code does.
The last post already proved the ordering, Start, End, Promise callback, Timer callback, with a step-through visualizer. No need to prove that again. What's worth proving next is sharper: a microtask having priority is not the same thing as a microtask being fast.
A Slow Microtask Still Blocks Everything Else
Microtasks run on the exact same single thread as everything else. If a microtask's own callback takes a long time to actually run, nothing else gets a turn until it finishes, a regular callback waiting in the queue included, timer or not.
Hit Run. Start and End print immediately, same as always. Then there's a real, noticeable pause, that 300ms loop actually running, before Resolved promise finally prints. Only after that does Timer callback show up, even though its own zero-second delay finished long before the promise's callback ever started.
That's the timer being delayed for a completely different reason than the blocking example from the last post. It's not synchronous code hogging the call stack this time, it's a microtask, and the effect on the timer is identical either way: the call stack has to be genuinely free before the event loop can move on, and a slow microtask keeps it just as busy as slow synchronous code would.
The mechanic is the same one the last post's visualizer walked through step by step, the event loop only ever moves on once the call stack is genuinely empty. Step through it again here as a reminder, then picture the "Promise callback" step simply taking 300ms longer.
Event Loop Visualizer
console.log('Start');
setTimeout(() => console.log('Timer callback'), 0);
Promise.resolve().then(() => console.log('Promise callback'));
console.log('End');Call Stack
Web APIs Environment
Microtask Queue
Callback Queue
Console Output
Script starts. The global execution context goes on the call stack.
The Practical Takeaway
Nothing about this is a bug. The timer's callback did exactly what it promised, it never ran before its delay elapsed. It just also never got a guarantee about running right at that delay, only after it, whenever the thread actually becomes free.
JavaScript timers were never built for precise timing, and this is exactly why. Anywhere a slow Promise callback might be running, whether from a huge synchronous computation or heavy data processing inside a .then(), a nearby timer can end up running later than its delay suggests, sometimes by a lot.
The Essentials
- Microtask priority is about order, not speed. A slow microtask still runs before a timer, it just also delays that timer by however long it takes.
- The call stack has to be genuinely empty before the event loop moves on, and a slow microtask occupies the call stack exactly like slow synchronous code does.
- A timer's delay is never a guarantee of exact timing, only a minimum, doubly true once Promises are involved.
Keep reading