Index

The Event Loop - Async Execution systems

Topic

In Javascript, despite the fact that all programs are run on a single thread promises and async functions allow you to handle asynchronous operations. The real question, is how does this work underneath the hood?

Conjecture

I believe that I might not even be using this term correctly. It might actually be called the call stack? But I think that essentially the execution engine in the browser (For most modern browsers, this is based on chromium v8), but the way the event loop works is when a promise is initialized it is pushed onto the call stack resolved promises are then pull off the stack? Honestly I can’t really explain it. How would you design this though. If you had to. I mean if you think about a javascript app. It isn’t really a sequential thing. There are layouts and then functions that are triggered by user interactions with the layout. So really whenever javascript is running it is all really just a bundle of events that are all occurring at the same time. Essentially there are a large number of events that are happening and each event requires a response in terms of a function invocation from the V8 engine. So then the question is how do you decide the order of execution? Would you put new events in a queue or would you put them on a stack?

Research

It seems like the event loop is essentially a sequence of queues that the javascript execution engine iterates through.

When I talk about the execution engine I’m talking about a javascript “runtime” in a browser this means V8 — in Chrome and Edge at least; Firefox uses SpiderMonkey and Safari uses JavaScriptCore. If you are running javascript on a server you might be using the node runtime, or deno or bun (Is bun a runtime and a package manager? Yes, and a bundler and a test runner on top of that. That seemed crazy to me at first, but all four jobs have to parse and resolve JavaScript anyway, so sharing one engine across them is what makes bun fast). Continuing on though there are a lot of concepts in javascript that contribute to the event loop. Another one is the call stack.

The Call Stack: Javascript is a single threaded language. This thread is synonymous with the call stack. It tracks what commands we are currently executing on the thread.

A stack trace is the current state of the call stack when an error occurs

A stack is used because as you execute a function linearly the nested commands and functions get pushed and popped off the stack until the parent function is complete. This works great until you have async functions that take a long time and start blocking the call stack. Because we are in the browser, a lot of things are happening at once, which is why callbacks exist: instead of blocking the stack while you wait for a result, you hand the runtime a function to run later and get out of the way.

This is where the event loop comes in. The browser is more than just the javascript runtime. It has built in WebAPIs and task queues. Whenever the javascript stack is empty the event loop pulls items from the task queue and pushes them onto the stack. Thus you can use a function like setTimeout to defer code to run until after the stack is clear.

The reality is that the callback queue isn’t the only queue in the event loop, and I had the order backwards. When the stack empties, the microtask queue drains first, and it drains completely — that is where resolved promises land, which is why a promise callback always runs before a setTimeout(fn, 0) that was scheduled before it. Only then does the browser get its chance to render. Only after that does the loop pull a single task off the macrotask queue (timers, I/O, user events), and then it drains microtasks again before the next one.

That ordering explains a failure mode I have actually hit: an endless chain of promises will lock up a page completely, because the microtask queue never empties and rendering never gets its turn. A runaway setTimeout loop won’t, because each timer is a separate macrotask with a render opportunity in between.

Final Summary

Essentially the event loop augments the traditional javascript language execution cycle. It allows async tasks to be resolved off of the call stack and then the callback functions returned to the call stack when it is empty. The thing I had wrong going in was the ordering — microtasks drain before rendering rather than after, and only one macrotask runs per turn of the loop. Worth noting too that the event loop is specified by HTML, not by the language: V8 supplies the stack and the microtask queue, the browser supplies everything else.