We use cookies to enhance your experience on the site
CodeWorlds

Event Loop (Event Model)

Welcome back to Jurassic Park! So far we have focused on the basics of object-oriented programming — building classes, inheritance, and composition. However, in real applications like a dinosaur park management system, we often have to deal with operations that take a lot of time — from fetching data from sensors in enclosures, communicating with security systems, to handling park visitor interactions.

In this module we will move on to a key aspect of JavaScript: its asynchronous nature and the event model it is built on. Understanding this mechanism is essential for building responsive and efficient applications that can handle many operations simultaneously — which is critical for the safety of a park full of predators!

JavaScript — A Single-Threaded Language with Asynchronous Capabilities

JavaScript is a single-threaded language. This means it can execute only one operation at a time. It is similar to having only one employee to handle all tasks in the park.

Imagine that this employee (the JavaScript thread) must:

  • Check the status of the fences
  • Monitor dinosaur vital signs
  • Respond to messages from guests
  • Control emergency systems

If they performed these tasks synchronously (one after another), they would have to fully complete one task before moving on to the next. That would be inefficient and potentially dangerous!

Fortunately, JavaScript has a clever system called the "event loop" that allows it to handle asynchronous operations despite being single-threaded.

Event Loop — The Heart of Asynchronous JavaScript

To understand the event loop, let's imagine the Jurassic Park control center. In this center we have:

  1. Call Stack — a list of currently executing tasks (the main tasks of our employee)
  2. Callback Queue — a list of tasks waiting to be executed after asynchronous operations complete
  3. Web APIs (in browsers) or C++ APIs (in Node.js) — external systems that can perform tasks "in the background"
  4. Event Loop — a mechanism that continuously checks whether the call stack is empty, and if so, moves tasks from the queue to the stack

Let's see how this works with a Jurassic Park example:

1console.log("1. Starting the morning park walkthrough.");
2
3setTimeout(() => {
4  console.log("4. Received data from dinosaur enclosure sensors.");
5}, 2000);
6
7fetch('https://jurassic-park-api.com/security-status')
8  .then(response => response.json())
9  .then(data => {
10    console.log("5. Security system status:", data.status);
11  });
12
13console.log("2. Checking today's staff roster.");
14console.log("3. Morning walkthrough complete.");

If we run this code, we will see in the console:

11. Starting the morning park walkthrough.
22. Checking today's staff roster.
33. Morning walkthrough complete.
44. Received data from dinosaur enclosure sensors.
55. Security system status: active

What happened? Let's analyze it step by step:

  1. console.log("1. Starting the morning park walkthrough.")
    is added to the call stack, executed, and removed from the stack.

  2. setTimeout(...)
    is added to the call stack. JavaScript recognizes this is an asynchronous call and passes it to the Web API, which starts the timer.
    setTimeout
    is removed from the stack.

  3. fetch(...)
    is added to the call stack. JavaScript recognizes this is an asynchronous call and passes it to the Web API, which sends the HTTP request.
    fetch
    is removed from the stack.

  4. console.log("2. Checking today's staff roster.")
    is added to the stack, executed, and removed.

  5. console.log("3. Morning walkthrough complete.")
    is added to the stack, executed, and removed.

  6. The call stack is now empty, but we have two asynchronous operations in progress:

    • The timer from
      setTimeout
    • The HTTP request from
      fetch
  7. After 2 seconds the timer expires and the

    setTimeout
    callback is added to the task queue.

  8. The event loop sees the call stack is empty, so it moves the callback from the queue to the stack.

  9. console.log("4. Received data from dinosaur enclosure sensors.")
    is executed and removed from the stack.

  10. Meanwhile the HTTP request completes, the data is fetched, and the

    .then()
    callback is added to the queue.

  11. The event loop moves the callback from the queue to the stack again.

  12. The data is processed and

    console.log("5. Security system status:", data.status)
    is executed.

Event Loop — Detailed Analysis

To better understand the event loop, let's draw its main components:

1┌─────────────┐    ┌───────────┐    ┌───────────────────┐
2│             │    │           │    │                   │
3│ Call Stack  │    │ Web APIs  │    │  Callback Queue   │
4│             │    │           │    │                   │
5└─────────────┘    └───────────┘    └───────────────────┘
6       ↑                 │                  │
7       │                 ↓                  │
8       │            ┌────────┐              │
9       └────────────│  Event │←─────────────┘
10                    │  Loop  │
11                    └────────┘

In the graphical representation:

  1. Call Stack — a LIFO (Last In, First Out) stack storing function calls currently being executed
  2. Web APIs — browser/environment interfaces for asynchronous operations (setTimeout, fetch, Event Listeners)
  3. Callback Queue — a FIFO (First In, First Out) queue storing callbacks ready to execute
  4. Event Loop — continuously checks whether the call stack is empty, and if so, moves functions from the queue to the stack

The amazing thing is that all of this runs on a single thread! JavaScript does not execute these tasks in parallel, but switches between them in a way that appears as if everything is happening simultaneously.

Microtasks and Macrotasks

In newer JavaScript implementations (and the specification) we distinguish two types of queues:

  1. Microtask Queue — for higher-priority tasks, such as Promise callbacks
  2. Macrotask Queue / Task Queue — for lower-priority tasks, such as setTimeout, setInterval, I/O

When the stack is empty, all tasks from the microtask queue are executed first, then from the macrotask queue.

1console.log("1. Starting the morning dinosaur feeding.");
2
3// Macrotask (setTimeout)
4setTimeout(() => {
5  console.log("4. Checking dinosaur health status.");
6}, 0);
7
8// Microtask (Promise)
9Promise.resolve().then(() => {
10  console.log("3. Reading data from the security system.");
11});
12
13console.log("2. Preparing food for the predators.");

Result:

11. Starting the morning dinosaur feeding.
22. Preparing food for the predators.
33. Reading data from the security system.
44. Checking dinosaur health status.

Notice that although both asynchronous tasks (

setTimeout
with 0ms and
Promise.resolve()
) are ready to execute almost immediately, the Promise (microtask) executes before the setTimeout (macrotask).

Blocking the Event Loop

Because JavaScript is single-threaded, long-running operations can "block" the event loop, stopping other tasks from executing. It is like the situation where the control center worker gets stuck at one monitor and cannot check the other systems.

1console.log("Starting dinosaur growth simulation...");
2
3// Function that blocks execution for 5 seconds
4function heavyComputation() {
5  const startTime = Date.now();
6  while (Date.now() - startTime < 5000) {
7    // Intensive computations...
8  }
9}
10
11heavyComputation();
12console.log("Simulation complete!"); // After 5 seconds
13
14// This code will not execute for 5 seconds, even though it is asynchronous!
15setTimeout(() => {
16  console.log("Checking security systems...");
17}, 1000);

In this example the

heavyComputation()
function blocks the JavaScript thread for 5 seconds. During this time no other tasks can be executed, including asynchronous ones. It is like the park worker getting stuck on one task and not being able to respond to alarms or other important signals.

In a real Jurassic Park such blocking could be catastrophic — a raptor could escape and the system would not register it because it is busy with something else!

That is why in JavaScript we try to:

  1. Avoid long synchronous operations
  2. Split complex computations into smaller pieces
  3. Use Web Workers for CPU-intensive operations

Task Execution Order — Practical Example

Let's analyze a more complex example that shows the order of execution of different task types in Jurassic Park:

1// Jurassic Park control system simulation
2console.log("1. Starting the Jurassic Park control system...");
3
4// Macrotask (setTimeout)
5setTimeout(() => {
6  console.log("6. Checking electric fences...");
7
8  // Nested microtask inside macrotask
9  Promise.resolve().then(() => {
10    console.log("7. Verifying predator status...");
11  });
12}, 0);
13
14// Microtask (Promise)
15Promise.resolve().then(() => {
16  console.log("3. Initializing security cameras...");
17
18  // Nested microtask
19  Promise.resolve().then(() => {
20    console.log("4. Calibrating motion sensors...");
21  });
22});
23
24// Another microtask
25Promise.resolve().then(() => {
26  console.log("5. Starting emergency protocols...");
27});
28
29// Synchronous code
30console.log("2. Checking system status...");

Result:

11. Starting the Jurassic Park control system...
22. Checking system status...
33. Initializing security cameras...
44. Calibrating motion sensors...
55. Starting emergency protocols...
66. Checking electric fences...
77. Verifying predator status...

This example shows that:

  1. Synchronous code executes first (1, 2)
  2. Then all microtask queue tasks execute (3, 4, 5)
  3. Only then do macrotask queue tasks execute (6)
  4. Microtasks created during a macrotask (7) execute immediately after the current macrotask finishes

Bonus: Var, Let, Const — When to Use Which?

When working with asynchronous code it is important to declare variables correctly. The way of declaration affects their behavior in blocks and functions, which directly impacts asynchronous code (e.g., variables in loops with setTimeout). Just as the park has different types of fences for different dinosaur types (electric for T-Rex, standard for Triceratops), in JavaScript we have different ways to declare variables:

var
,
let
, and
const
.

Const — Always the Default Choice

const
means a constant — a value that cannot be reassigned. In Jurassic Park we use
const
for things that should not change, like safety parameters or system configuration:

1// System configuration — should not change
2const MAX_DINOSAURS_PER_ENCLOSURE = 5;
3const FENCE_MINIMUM_VOLTAGE = 10000; // volts
4const EMERGENCY_CONTACT = "+1-555-JURASSIC";
5
6// Trying to change a const will throw an error
7MAX_DINOSAURS_PER_ENCLOSURE = 10; // TypeError: Assignment to constant variable

When to use const:

  • For values that will not change
  • For objects and arrays (even if their contents change)
  • As the default choice — when in doubt, use
    const

Important:

const
does not mean the contents of an object or array are immutable!

1// This is fine — we modify the contents, not the assignment
2const dinosaurs = [];
3dinosaurs.push({ name: "Rexy", species: "T-Rex" }); // OK!
4dinosaurs.push({ name: "Blue", species: "Velociraptor" }); // OK!
5
6const parkConfig = {
7  openTime: "09:00",
8  closeTime: "18:00"
9};
10parkConfig.closeTime = "20:00"; // OK — modifying a property
11
12// This will throw an error — trying to reassign
13dinosaurs = []; // TypeError!
14parkConfig = {}; // TypeError!

Let — When the Value Changes

let
is used when a variable's value will change during program execution. In Jurassic Park we use
let
for dynamic things like counters, accumulators, or loop variables:

1// Visitor counter — changes throughout the day
2let visitorCount = 0;
3
4function addVisitors(count) {
5  visitorCount += count;
6  console.log(`Current visitor count: ${visitorCount}`);
7}
8
9addVisitors(50); // 50
10addVisitors(30); // 80
11addVisitors(20); // 100

When to use let:

  • In loops (for, while)
  • For counters and accumulators
  • When the value will be reassigned
1// Loops — classic use case for let
2for (let i = 0; i < 5; i++) {
3  console.log(`Checking dinosaur #${i}`);
4}
5
6// Accumulators
7let totalFeedingCost = 0;
8
9const dinosaurs = [
10  { name: "Rexy", dailyFoodCost: 500 },
11  { name: "Blue", dailyFoodCost: 150 },
12  { name: "Trixie", dailyFoodCost: 200 }
13];
14
15for (const dino of dinosaurs) {
16  totalFeedingCost += dino.dailyFoodCost;
17}
18
19console.log(`Total feeding cost: $${totalFeedingCost}`);

Var — Almost Never (Legacy Code)

var
is the old way of declaring variables in JavaScript. It has problematic behavior and should not be used in modern code. The only time you will see
var
is in old (legacy) code written before 2015 (before ES6).

Problems with var:

  1. Function scope instead of block scope
    var
    "escapes" from blocks
1// Problem: var ignores blocks
2function checkDinosaur(isDangerous) {
3  if (isDangerous) {
4    var alert = "DANGER!"; // var is accessible outside the if block!
5  }
6
7  console.log(alert); // "DANGER!" — leaks out of the if block!
8  // With let/const we would get a ReferenceError
9}
10
11checkDinosaur(true);
12
13// Compare with let:
14function checkDinosaurProper(isDangerous) {
15  if (isDangerous) {
16    let alert = "DANGER!";
17  }
18
19  console.log(alert); // ReferenceError: alert is not defined
20}
  1. Hoisting with undefined
    var
    is "hoisted" to the top of the function
1// Strange var behavior
2function feedDinosaurs() {
3  console.log(food); // undefined (not ReferenceError!)
4  var food = "meat";
5  console.log(food); // "meat"
6}
7
8// This is interpreted as:
9function feedDinosaurs() {
10  var food; // hoisting — declaration at the top
11  console.log(food); // undefined
12  food = "meat"; // assignment
13  console.log(food); // "meat"
14}
  1. Can be redeclared — you can accidentally overwrite variables
1var dinosaurName = "Rexy";
2// Accidentally using the same name
3var dinosaurName = "Blue"; // No error! Overwrote the previous value
4
5console.log(dinosaurName); // "Blue" — Rexy is gone!
6
7// With let/const:
8let dinoName = "Rexy";
9let dinoName = "Blue"; // SyntaxError: Identifier 'dinoName' has already been declared

When to use var:

  • Never in new code
  • Only when editing very old code and you must maintain compatibility

Practical Example — Code Review

Imagine you receive code from a colleague for review. Here is how to improve variable usage:

Bad code (before):

1// Bad — everything with var
2var parkName = "Jurassic Park"; // Does not change — should be const
3var isOpen = true; // Changes — should be let
4var maxCapacity = 10000; // Does not change — should be const
5
6function processVisitors() {
7  var totalVisitors = 0; // Accumulator — should be let
8
9  for (var i = 0; i < 100; i++) { // Iterator — should be let
10    totalVisitors++;
11  }
12
13  console.log(i); // 100 — var leaks! Problem!
14  return totalVisitors;
15}

Good code (after):

1// Good — using const as default
2const parkName = "Jurassic Park";
3const maxCapacity = 10000;
4
5// let only when the value changes
6let isOpen = true;
7
8function processVisitors() {
9  // let for accumulator
10  let totalVisitors = 0;
11
12  // let in loop — block scoped
13  for (let i = 0; i < 100; i++) {
14    totalVisitors++;
15  }
16
17  // console.log(i); // ReferenceError — i is scoped to the loop
18  return totalVisitors;
19}
20
21// We can change isOpen
22isOpen = false;
23isOpen = true;
24
25// But we cannot change const
26// parkName = "Dino World"; // TypeError!

Another Example — Dinosaur Management System

1class DinosaurManagementSystem {
2  constructor() {
3    // const for properties whose references do not change
4    this.dinosaurs = []; // array contents may change, but reference does not
5    this.config = {     // object contents may change, but reference does not
6      maxDinosaurs: 50,
7      securityLevel: "high"
8    };
9  }
10
11  addDinosaur(name, species, dangerLevel) {
12    // const for function parameters and local values that do not change
13    const newDinosaur = {
14      id: Date.now(),
15      name,
16      species,
17      dangerLevel,
18      status: "active"
19    };
20
21    this.dinosaurs.push(newDinosaur);
22    return newDinosaur;
23  }
24
25  getDangerousCount() {
26    // let for accumulator
27    let count = 0;
28
29    // const in for...of when we do not modify the loop variable
30    for (const dino of this.dinosaurs) {
31      if (dino.dangerLevel > 7) {
32        count++; // we modify count, hence let
33      }
34    }
35
36    return count;
37  }
38
39  calculateDailyFoodCost() {
40    // let for values that change
41    let totalCost = 0;
42
43    // let in classic for loop
44    for (let i = 0; i < this.dinosaurs.length; i++) {
45      const dino = this.dinosaurs[i]; // const — we do not reassign the reference
46
47      // const for computed values
48      const dailyCost = this.calculateDinosaurFoodCost(dino);
49      totalCost += dailyCost;
50    }
51
52    return totalCost;
53  }
54
55  calculateDinosaurFoodCost(dinosaur) {
56    // const for all values that do not change
57    const baseCost = 50;
58    const costMultiplier = dinosaur.dangerLevel > 5 ? 2 : 1;
59    const dailyCost = baseCost * costMultiplier;
60
61    return dailyCost;
62  }
63}
64
65// Usage example
66const parkSystem = new DinosaurManagementSystem(); // const — reference does not change
67
68parkSystem.addDinosaur("Rexy", "T-Rex", 9);
69parkSystem.addDinosaur("Blue", "Velociraptor", 8);
70parkSystem.addDinosaur("Trixie", "Triceratops", 3);
71
72// let for values that will change
73let dangerousCount = parkSystem.getDangerousCount();
74console.log(`Dangerous dinosaurs: ${dangerousCount}`);
75
76// const for one-time-use values
77const foodCost = parkSystem.calculateDailyFoodCost();
78console.log(`Daily food cost: $${foodCost}`);
79
80// We can add more dinosaurs
81parkSystem.addDinosaur("Delta", "Velociraptor", 8);
82
83// And recalculate
84dangerousCount = parkSystem.getDangerousCount(); // let allows reassignment
85console.log(`Dangerous dinosaurs: ${dangerousCount}`);

Rule of Thumb

  1. Always start with
    const
    — if the compiler/linter complains that you are trying to change the value, switch to
    let
  2. Use
    let
    only when you have to
    — loops, counters, accumulators
  3. Never use
    var
    — unless editing old code
1// ✅ Good practice
2const maxDinosaurs = 50;
3let currentDinosaurs = 0;
4
5for (let i = 0; i < maxDinosaurs; i++) {
6  const dinosaur = createDinosaur();
7  currentDinosaurs++;
8}
9
10// ❌ Bad practice
11var maxDinosaurs = 50;
12var currentDinosaurs = 0;
13
14for (var i = 0; i < maxDinosaurs; i++) {
15  var dinosaur = createDinosaur();
16  currentDinosaurs++;
17}

Why Does This Matter?

Just as proper fences in Jurassic Park prevent dinosaur escapes, proper use of

const
and
let
prevents bugs in code:

  1. Readability — you can immediately see which values change (
    let
    ) and which do not (
    const
    )
  2. Safety
    const
    protects against accidental modification
  3. Block scope — variables do not "leak" out of blocks
  4. Easier debugging — fewer surprises from hoisting

Remember: in modern JavaScript,

var
is like dinosaurs — an interesting relic of the past, but you do not want it in your production code!

Summary

The JavaScript event model (event loop) is the key to understanding how this single-threaded language can handle many asynchronous operations — essential in applications like the Jurassic Park management system.

Remember the key principles:

  1. JavaScript is single-threaded but asynchronous
  2. The event loop allows handling many tasks without blocking the user interface
  3. Tasks execute in a defined order: synchronous → microtasks → macrotasks
  4. Avoid blocking the event loop with long synchronous operations

In the next lessons we will see how to use this knowledge to write efficient asynchronous code using callbacks, Promises, and async/await.

Go to CodeWorlds