Skip to Content

Event loop

2017-11-01

While learning JavaScript, you might've heard that JS is a single-threaded language. You also might've heard that JS is asynchronous. These two statements probably left you thinking "Wait, what? JS can't really be asynchronous with just one thread, right?". Let's look into how this works.

Both browsers and Node.js use a runtime engine to run JavaScript (in case of Chrome and Node.js it's the V8). Inside that engine (besides all the other stuff that makes JS actually work) is the stack. Every time a function is called it goes on top of the stack; every time we return from the function (either through return or by reaching the end of the function) it gets removed from the top of the stack. That basically means that we can only execute one thing at the time. That's pretty bad, considering that the page re-render also happens through the stack and can be blocked, meaning the user won't be able to interact with the page while there's still stuff in the stack.

Luckily, both browsers and Node.js provide us with asynchronous APIs (usually written in C++) that are executed outside of the runtime engine. Whenever an asynchronous call is made (e.g. setTimeout, an AJAX-request or an onClick event) it gets pushed to these APIs, which can run in multiple threads. After that function's execution is finished, its callback handler is placed in the callback queue. This is where the event loop comes into play. Its only job is to check the callback queue every once in a while, and place the next function in the queue on top of the stack whenever the stack is empty and the callback queue isn't.

Here's a visualization of how it all works from Philip Roberts' speech from JSConf EU 2014

What I think should be noted is the way setTimeout actually works. Calling setTimeout(function foo(){...}, 5000) doesn't mean foo will be executed in precisely 5 seconds; it means that in 5 seconds foo will be put into the callback queue, where it will wait until the stack is empty. This is why sometimes setTimeout(..., 0) is used: instead of just executing its callback immediately, it waits until all the other stuff is done.

While being very similar to its in-browser counterpart, the event loop within Node.js is tad a bit more complex. In Node, the event loop is separated into several phases, each handling it's own operations. These phases are:

  • timers: handles the callbacks of setTimeout and setInterval by setting thresholds after which they can be executed (but the execution itself is actually handled by the poll phase)
  • I/O callbacks: deals with all other callbacks (except for the ones handled by the previous phase, created by setImmediate and close callbacks)
  • idle, prepare: used for internal Node stuff
  • poll: executes the callbacks with expired thresholds and processes whatever is in the callback queue
  • check: exclusively takes care of setImmediate's callbacks
  • close callbacks: manages the close events on sockets

Another mechanism Node.js features is the process.nextTick() function. The way it works is: it accepts a callback and a number of arguments to pass to that callback; it then procedes to invoke the callback before the event loop continues (but still after all synchronous operations) without waiting for the appropriate phase, completely disregarding the event loop's current state. It can cause some serious problems (e.g. if you call it recursively, in which case the event loop will never reach the poll phase, completely disabling I/O), and you might never actually use it in your practice, but it can sometimes prove useful when processing something immediately is essential, or when you need your code to run after everything in the call stack gets processed but before the event loop continues (e.g. if you emit an event before that event's handler is declared you can wrap the emit function in process.nextTick so it waits until all everything is declared). For other cases, the official Node.js documentation recommends using setImmediate instead, since it provides compatibility with in-browser JS and causes less potential problems.

In short, there are some thing in JavaScript that should be done asynchronously, and since the runtime engines are single-threaded, they use other APIs that are capable of working in parallel.

If you're still having trouble understanding how the event loop works or just want to play around with it, there's a resource called loupe (also created by Philip Roberts) that visualizes JavaScript code and shows how the stack, the WebAPIs, the callback queue and the event loop interact with each other.

Author: Nikita Berilov

Mifort, Mifort-blog, Mifort-articles, Web Development, JavaScript, Event loop