Sitemap

Understanding JavaScript’s Execution Model: Agents, Threads, and Realms

8 min read3 days ago

--

Press enter or click to view image in full size

Understanding how JavaScript manages execution, memory, and communication between agents using a simple warehouse analogy.

While learning about ECMAScript, I came across something called the JavaScript agent execution model.

I knew about JavaScript functions, objects, and other programming concepts, but I hadn’t really explored how these things are managed when JavaScript executes code.

That’s when I came across terms like agents, threads, call stacks, heaps, job queues, workers, and realms.

Initially, I was trying to understand each concept separately. But then I thought, instead of just remembering their definitions, why not connect them through one example?

Let’s take a warehouse and its delivery operations.

What Is a JavaScript Agent?

Imagine we’re running a delivery company operating across multiple districts.

Each district has its own delivery office responsible for handling orders, maintaining records, and completing deliveries.

Let’s consider one district delivery office as an agent.

Now, what does this office need to operate?

It needs someone to execute the work, somewhere to maintain information about orders, a way to track what it’s currently doing, and a place to keep pending work.

JavaScript agents have similar responsibilities.

An agent is an independent unit for executing JavaScript code. It has several components associated with its execution:

  • Execution thread: Executes JavaScript instructions.
  • Call stack: Keeps track of active function calls.
  • Heap: Holds objects and other runtime data.
  • Job queue: Holds jobs waiting to execute.
  • Realm: Provides the global environment and built-in objects used by JavaScript code.

Of course, an agent isn’t literally a warehouse or physical machine. This is just an analogy to understand how everything connects.

Now let’s understand each component.

JavaScript Agent vs. Thread: What’s the Difference?

Imagine our district office receives ten delivery orders.

There’s one delivery executor responsible for processing those orders.

In our analogy, that executor represents the execution thread.

Here’s something important.

An order is a task. The delivery executor is the person performing that task.

Similarly, in JavaScript, a task and a thread aren’t the same thing.

A thread executes instructions, while a task represents work that needs to be performed.

When I first encountered agents, I pictured an agent as a machine assigned to execute a task.

But an agent is more than a thread. It includes the execution structures and memory needed to run JavaScript code.

And whenever JavaScript calls a function, it doesn’t create a new thread for that function.

Instead, the function executes through the existing agent’s execution thread.

This brings us to the call stack.

How Does the JavaScript Call Stack Work?

Let’s continue with the same delivery example.

Before delivering an order, we need to verify the customer’s address.

And to verify the address, we need to check the PIN code.

Consider this JavaScript code:

function deliverOrder() {
verifyAddress();
}
function verifyAddress() {
checkPincode();
}
function checkPincode() {
console.log("Pincode verified");
}
deliverOrder();

Now, what actually happens when we call deliverOrder()?

First, deliverOrder() enters the call stack.

Then it calls verifyAddress(), so that function is placed on top of it.

Next, verifyAddress() calls checkPincode().

Our call stack now looks something like this:

CALL STACK
    ┌──────────────────┐
│ checkPincode() │ ← Top
├──────────────────┤
│ verifyAddress() │
├──────────────────┤
│ deliverOrder() │
└──────────────────┘

Now, suppose checkPincode() finishes.

What happens next?

It gets removed from the call stack, and execution returns to verifyAddress().

Once verifyAddress() finishes, it gets removed, and execution returns to deliverOrder().

So, in this example, the last function added to the stack is the first one removed.

This follows the Last In, First Out (LIFO) principle.

One important thing to understand here is that every function call creates a new stack frame, not a new thread.

We still have one execution thread managing these function calls.

Understanding Heap Memory in JavaScript

Now that we understand how functions execute, let’s talk about where objects are stored.

Our delivery office needs to maintain information about orders.

Let’s say we have an order:

const order = {
orderId: 101,
customer: "Rahul",
status: "pending"
};

We can imagine the heap as a storage area in our warehouse, where information about orders is maintained.

In JavaScript’s conceptual execution model, the heap is memory used to store objects and other dynamically allocated data.

Now imagine that the delivery is completed.

function deliverOrder() {
order.status = "delivered";
}
deliverOrder();

What happens here?

We already have an order object.

We aren’t creating another order just because its status changes.

Instead, we’re updating the existing object’s status property from "pending" to "delivered".

So we can think about the difference this way:

The call stack tracks which functions are executing, while the heap holds objects that those functions can access and modify.

One small technical detail: actual JavaScript engines can optimize how values are stored. The stack-and-heap model helps us understand execution, but it isn’t an exact description of how every value is physically stored.

What Is the JavaScript Job Queue?

Let’s return to the district delivery office.

Imagine our delivery executor is already processing an order.

Meanwhile, another order becomes ready for processing.

Where does this new work go?

It needs a way to wait until the executor can process it.

This is where the idea of a job queue comes in.

A JavaScript agent maintains queues of jobs waiting to execute.

When a job is selected for execution, it uses the agent’s execution thread and call stack.

Now, the important distinction is between the call stack and the job queue.

The call stack tracks the functions involved in executing the current work.

The job queue holds jobs that are ready but haven’t started executing yet.

They serve different purposes.

The full browser execution model also involves an event loop, tasks, and microtasks. That’s a broader topic, but for now, this distinction helps us understand why both the call stack and queues exist.

So our agent now has a thread to execute code, a stack to track function calls, a heap for objects, and queues for scheduled work.

But there’s another concept I came across while learning about agents.

Get Dhyuthiwrites’s stories in your inbox

Join Medium for free to get updates from this writer.

It’s called a realm.

What Is a JavaScript Realm?

This concept was a little confusing initially.

If an agent already has a thread, stack, heap, and queue, why does it also need a realm?

The way I started understanding it was that a realm provides the global environment and built-in objects that JavaScript code uses.

For example, JavaScript provides built-in objects such as:

Array
Object
Promise
Math

A realm has its own global object, associated environment, and set of ECMAScript intrinsic objects.

Let’s relate it to our warehouse.

Imagine each delivery office has an operating setup with facilities and resources available for doing its work.

That’s roughly how I think about a realm.

But there’s an important difference between a realm and an agent.

A realm doesn’t execute JavaScript code independently.

It isn’t another thread.

In fact, one agent can have multiple realms.

For example, a webpage and a same-origin iframe can have separate realms while belonging to the same agent.

Each realm has its own global environment and built-in object identities.

But that doesn’t mean both realms automatically execute JavaScript in parallel.

They still use the execution thread belonging to their agent.

Another point worth understanding is that ECMAScript’s built-in objects aren’t the same as all the APIs provided by a browser.

For example, browser features like DOM manipulation come from the host environment.

So a realm provides the core JavaScript environment, while the host can make additional capabilities available.

JavaScript Agents vs. Web Workers

Now let’s return to our delivery company.

So far, we’ve been discussing one district delivery office.

But what if we have multiple district offices?

District A has its own executor, and District B has another executor.

Both offices can independently handle their own work.

This gives us a way to think about multiple JavaScript agents.

In browsers, one way to create independent JavaScript execution is through Web Workers.

The main webpage has an agent, and a dedicated Web Worker runs in another agent.

The worker has its own execution thread, stack, heap, and realm.

It can perform work independently of the main webpage’s JavaScript execution.

But a worker isn’t simply another realm within the same agent.

That’s the distinction.

A realm provides a global language environment.

A Web Worker provides a separate execution context through another agent.

Workers also have limitations. For example, an ordinary Web Worker cannot directly manipulate the webpage’s DOM.

I haven’t explored building Web Workers in detail yet, so I’ll leave the implementation side for another article.

But there’s one more interesting question.

If two agents execute JavaScript independently, how do they communicate?

How Do JavaScript Agents Communicate?

Let’s say District A needs District B to process an order.

There are two communication approaches I came across while studying the execution model:

Message passing and shared memory.

Let’s understand the basic difference.

1. Message Passing Between Agents

Suppose District A maintains this order:

const order = {
orderId: 101,
status: "pending"
};

Now District A wants District B to process that order.

One way is to send the order information as a message.

In JavaScript, postMessage() is commonly used to communicate with Web Workers.

When an ordinary JavaScript object is sent this way, the receiving worker gets a separate copy through structured cloning.

Now imagine District B updates the received order’s status to "delivered".

Will District A’s original object automatically change?

No.

District B modified its own copy.

If District A needs the updated status, District B can send another message back, and District A can update its own order record.

That’s the basic idea behind message passing.

There are also transferable resources and shared-memory objects, so we shouldn’t assume that every message always copies all underlying data.

2. Shared Memory Between Agents

Now let’s imagine a different situation.

Instead of maintaining separate copies of certain information, District A and District B both need access to the same storage area.

Both offices should be able to read and update information there.

This is similar to shared memory.

In JavaScript, SharedArrayBuffer allows eligible agents to access the same underlying block of memory.

Each agent maintains its own ordinary heap, but certain underlying memory bytes can be shared.

This is different from message passing.

With ordinary message passing, one agent sends information to another.

With shared memory, multiple agents can access the same underlying memory.

Of course, this introduces additional challenges.

What happens if two agents try to update the same information at nearly the same time?

How do we prevent them from interfering with each other?

These questions lead to concepts like race conditions and atomic operations, which deserve a separate discussion.

Shared memory is also subject to browser security restrictions, so it isn’t automatically available between every pair of agents.

For now, the important idea is that message passing and shared memory provide different ways for agents to communicate.

Putting Everything Together

Let’s return to our original warehouse example.

We started with a district delivery office and considered it as an agent.

Inside that office, we have an executor representing the execution thread.

The call stack keeps track of active function calls.

The heap holds objects that functions can access and update.

Job queues manage scheduled work waiting to execute.

A realm provides the global environment and built-in objects that JavaScript code uses.

And when we need another independent executor, browser environments provide workers that run in separate agents.

Those agents can communicate through messages or, when permitted, shared memory.

The main thing that became clearer to me was that an agent isn’t simply another name for a thread.

An agent brings together the execution structures and environment needed to run JavaScript.

Once I started connecting these concepts through one example, the execution model became easier to understand.

I’m still exploring the practical side of workers, shared memory, and concurrency. Understanding the concepts is one step; experimenting with them is another.

That’s what I’d like to explore next.

References

--

--