Scope, hoisting, event loop: The event loop

The Event Loop is one of the most important aspects to understand about JavaScript.

This section aims to explain you why sometimes some functions are executed before others, even though they are written in a different order.

Your JavaScript code runs single threaded. There is just one thing happening at a time.

This is a limitation that’s actually very helpful, as it simplifies a lot how you program without worrying about concurrency issues, which are a can of worms.

You just need to pay attention to how you write your code and avoid anything that could block the thread, like synchronous network calls or infinite loops.

In browsers every tab is isolated and there is an event loop for every tab, so you avoid a web page with infinite loops or heavy processing to block your entire browser and affect other pages you are browsing.

The environment then can run things in the background, like API and Web Workers.

You mainly need to be concerned that your code runs on a single event loop, and write code with this thing in mind to avoid blocking the loop, in order for your app to be performant.

Any JavaScript code that takes too long to return back control to the event loop will block the execution of any JavaScript code in the page, even block the UI thread, and the user cannot click around, scroll the page, and so on.

Almost all the APIs offered by browsers and Node.js in JavaScript are non-blocking.

As I mentioned API requests, also filesystem operations in Node.js, and so on.

Being blocking is the exception, and this is why JavaScript is based so much on callbacks, and more recently on promises and async/await.

More about those topics soon.

Let’s talk about the call stack.

The call stack is a LIFO queue (Last In, First Out).

Every time we call / invoke a function, it’s put on the call stack.

The event loop has the job of continuously checking the call stack to see if there’s any function that needs to run, and executes each one in order.

Let’s do an example to show how the event loop works in practice.

const a = () => console.log('a')

const b = () => console.log('b')

const c = () => {
  console.log('c')
  a()
  b()
}

c()

This code prints

c
a
b

as expected.

When this code runs, first c() is called by the program. Inside c() we first call a(), then we call b().

The event loop on every iteration looks if there’s something in the call stack, and executes it until the call stack is empty.

The above example looks normal, there’s nothing special about it: JavaScript finds things to execute, runs them in order.

Things are working as expected, until we mix in asynchronous code, like setTimeout calls, or promises.

We saw setTimeout() in the previous section.

There’s a special thing happening with it, even when we set the delay to 0 with setTimeout(() => {}), 0).

In this case we execute the callback function once every other function in the current function has executed.

Take this example:

const b = () => console.log('b')

const c = () => console.log('c')

const a = () => {
  console.log('a')
  setTimeout(b, 0)
  c()
}

a()

This code prints, maybe surprisingly:

a
c
b

When this code runs, first a() is called. Inside a() we first call setTimeout, passing b as an argument, and we instruct it to run immediately as fast as it can, passing 0 as the timer. Then we call c().

And we have some console.log() calls to help us figure out what is happening.

See, the order of execution is a, c, b.

Why is this happening?

When setTimeout() is called, the Browser (or Node.js) start the timer.

Once the timer expires, in this case immediately as we put 0 as the timeout, the callback function is put in the Message Queue.

The Message Queue is also where user-initiated events like click or keyboard events, or fetch() network calls responses are queued before your code has the opportunity to react to them.

The loop gives priority to the call stack, and it first processes everything it finds in the call stack, and once there’s nothing in there, it goes to pick up things in the message queue.

Promises are a bit different. They work with a Job Queue, which is a way to execute the result of an async function as soon as possible, rather than being put at the end of the call stack, like it happens with setTimeout().

Promises that resolve before the current function ends will be executed right after the current function.

I find nice the analogy of a rollercoaster ride at an amusement park: the message queue puts you at the back of the queue, behind all the other people, where you will have to wait for your turn, while the job queue is the fastpass ticket that lets you take another ride right after you finished the previous one.

Example:

const b = () => console.log('b')

const c = () => console.log('c')

const a = () => {
  console.log('a')
  setTimeout(b, 0)
  new Promise((resolve, reject) =>
    resolve('should be right after c, before b')
  ).then(resolve => console.log(resolve))
  c()
}

a()

This prints

a
c
should be right after c, before b
b

Lessons in this unit:

0: Introduction
1: Global scope
2: Function scope
3: Block scope
4: Shadowing
5: Hoisting
6: Closures
7: An issue with `var` variables and loops
8: ▶︎ The event loop
Are you intimidated by Git? Can’t figure out merge vs rebase? Are you afraid of screwing up something any time you have to do something in Git? Do you rely on ChatGPT or random people’s answer on StackOverflow to fix your problems? Your coworkers are tired of explaining Git to you all the time? Git is something we all need to use, but few of us really master it. I created this course to improve your Git (and GitHub) knowledge at a radical level. Launching May 21, 2024. Join the waiting list!