
Thought experiment
Take a guess at what happens when you click the button:

Will the element flash slightly on the UI before disappearing or will it not appear at all? Should we set the display to none before appending the element? Does it make a difference?
You can write JavaScript for years without thinking about the event loop, the same way you can drive for years without knowing what a transmission does.
Most of us get in the car, turn the key, and go from point A to point B. We never think about the engine, the gearbox, or what's happening under the hood, until the car breaks down on the highway, or starts making a noise that wasn't there yesterday. Then suddenly we wish we knew a little more.
And even short of breakdowns, the drivers who understand their car a level deeper are the ones who get better mileage and fewer surprises.
The event loop is your engine. You don't need to know it to write JavaScript, but the moment something behaves strangely, you're staring right at it.
The main thread
Everything the browser does on a page, running your JavaScript, responding to clicks, calculating styles, laying out elements, painting pixels, happens on a single thread called the main thread. There's only one of it, and it can only do one thing at a time.
If your script could mutate the DOM while the browser was painting it, or if two click handlers could run in parallel and both modify the same element, the web would be a minefield of race conditions. Single-threading keeps things predictable.
But it also means the main thread is a bottleneck: every millisecond it spends running your code is a millisecond it can't spend responding to the user or updating the screen. A loop that takes 200ms to finish is 200ms of frozen UI, meaning no scrolling, no clicking and no animation.
And the web is full of work that takes time: fetching from an API can take seconds, or a timer fires five minutes from now . Any of these would freeze the page if they ran on the main thread.
The browser can't afford to sit and wait. So instead, it hands those slow operations off to Web APIs that run outside the main thread like the network stack and event listeners, and lets your scripts keep going. When the work finishes, the result comes back as a callback waiting to be run. The event loop is the orchestrator of that operation.
The building blocks
The model has four moving parts:
If you want to watch this in motion, Philip Roberts’ Loupe visualizer lets you step through code and see frames pushed onto the stack and callbacks dropped into the queue in real time.
The call stack is where your code actually runs, every function call pushes a frame on, every return pops one off, and while there's anything on the stack, the main thread is busy and nothing else can happen. When your code hits an asynchronous operation like setTimeout, fetch, or addEventListener, it doesn't wait on the stack. It hands the work to a Web API.
Web APIs are a capability provided by the browser itself, running outside the main thread, and the stack carries on. When that work finishes (the timer fires, the response arrives, the user clicks), the Web API drops a callback into one of three queues that have different execution priorities and times.
The macrotask queue holds callbacks from setTimeout, DOM events, and I/O.
The animation queue holds requestAnimationFrame callbacks, scheduled to run just before the browser paints.
The microtask queue holds promise continuations and anything passed to queueMicrotask.
The event loop is the scheduler that ties it all together. Each cycle, it waits for the call stack to be empty, then drains the entire microtask queue, then runs any due animation callbacks and lets the browser paint, then pulls a single macrotask off its queue and runs it, and around it goes.
Microtasks are greedy (the whole queue is emptied every cycle, including any new ones added while draining), animation callbacks are paint-synced, and macrotasks are rationed one per turn. That asymmetry is why a resolved promise always beats a setTimeout(fn, 0) scheduled before it.
Back to the thought experiment
The paragraph never flashes onto the screen. And it makes no difference whether you set display: none before or after appending the element. Both versions behave identically.
The click handler is one synchronous function. It pushes onto the call stack, creates the element, appends it, hides it, and returns, all in one go. The browser cannot paint until the call stack is empty, and by the time it is, the paragraph's display is already none. There was never a moment in the event loop where the page could be rendered with a visible paragraph. The order of those two lines is irrelevant because neither of them is observable until the function returns.
However there is a way to make it flash:

The setTimeout callback gets queued as a macrotask. The handler returns, the call stack empties, the browser gets its turn to paint with the paragraph visible. Then the event loop pulls the macrotask and hides it. Same code, same zero-millisecond delay, completely different visual outcomes.
Knowledge Sharing is one of our values, and it shows up in our internal Tech Workshops, where engineers teach each other what they know. Here, we're turning those sessions into stories worth sharing.
Tech Lead

Book a call and get matched with engineers in 24–72h.