---
title: "04 — JavaScript — The Behavior Layer That Makes a Page React"
uid: javascript-the-language-of-the-web
tags: ["event-loop", "dom", "async", "roadmap:frontend", "fundamentals", "javascript", "events"]
excerpt: "JavaScript is the behavior layer: HTML is meaning, CSS is looks, JS is what the page does — single-threaded on an event loop."
date: 2026-08-12T18:35:12+0000
source: https://www.aveshina.my.id/en/blog/javascript-the-language-of-the-web
---

"The thing that makes buttons clickable" was my JavaScript summary, and it undersold the whole layer. The model that stuck: **JavaScript is the behavior layer. Where HTML is meaning and CSS is looks, JS is what the page *does*.** [1]

In the last two posts I had the static half of the picture. HTML declares what each piece of content is; CSS decides how it looks. The result is a page you can read but not affect — it sits there, fixed, the same for everyone. JavaScript is what turns that into something that reacts: a button that counts, a form that validates, a list that loads more as you scroll. It runs in the same browser that built the DOM from the HTML, and its core job is to read and change that tree in response to events.

The part that straightened everything else out is the *shape* of how it runs. JS is **single-threaded** — one instruction at a time, on one call stack — and it clears that stack on an **event loop** [2]. That single fact is why JS feels different from most languages I'd met: it's a language of values, scopes, and async, organized entirely around not blocking that one thread.

## Values and types — the stuff JS works in

JavaScript has a small set of **primitive types** — string, number, boolean, null, undefined, symbol, bigint — and one structural type: object [1]. The primitives are passed by value; objects (including arrays and functions) are passed by reference. I used to trip on this constantly — mutating an object inside a function and being surprised the caller saw the change. It stops being surprising once you accept that a variable holding an object holds a *reference* to it, not the thing itself.

Two quirks worth carrying in your head, because they eat debugging time:

- **Dynamic typing.** A variable can hold a string now and a number next line. There's no compile-time type gate; typeof x is the runtime check. TypeScript adds that gate as a separate layer, but plain JS trusts you to know what's in the box.
- **Type coercion.** == compares after converting types (0 == '' is true); === compares without. I default to === everywhere and let == alone, because its rules are a noise source, not a feature.

## The DOM bridge — where JS meets the page

The browser already parsed the HTML into a tree of nodes — the DOM, the same tree I leaned on for semantics and accessibility in the HTML post. JavaScript's bridge to the page is that it can **read and mutate that tree** [3]. document.querySelector finds a node; node.textContent changes its text; node.classList.add toggles a class; document.createElement makes a new node and parent.appendChild wires it in.

The thing I had to internalize: mutating the DOM is not the same as re-rendering. The browser keeps the live tree; I'm editing nodes in place. Frameworks like React hide this behind a virtual tree and a diff — but underneath, the same DOM is being updated node by node. The DOM is the shared substrate that *all* frontend code, framework or vanilla, ultimately touches.

## Events — how the page learns something happened

A static page doesn't need to know anything. A reactive page does — that a button was clicked, a key was pressed, a form submitted, data arrived. The browser is the one that *knows*; JS registers **handlers** and the browser calls them [4]:

```
button.addEventListener('click', () => {
  counter.textContent = String(count++);
});
```

The model here is important: I don't write code that polls for clicks. I hand the browser a function and say "call this when X happens." The browser maintains an internal queue of events; when the stack is empty, the event loop pulls the next event off the queue and runs its handler. That's the whole interaction model — register, then wait. It's also the entry point for everything async.

## Async — callbacks, promises, and async/await

Here's where the single thread bites. If my handler does something slow — fetches from a server, reads a large file — the whole page freezes until it returns, because nothing else can run on that one thread. So JS offloads waiting to the *environment* (the browser's network stack, timers) and gives me ways to say "do this, and when it's done, run this."

That evolved in three layers, and the layers are still all there:

- **Callbacks** — pass a function to be called later. Works, but nesting them produces the "pyramid of doom," and error handling is ad hoc.
- **Promises** — an object representing a value that will arrive later, with .then() chaining and a single .catch() for the whole chain [5]. Composable in a way callbacks aren't.
- **async/await** — syntactic sugar over promises that lets me write async code that *reads* top to bottom, with try/catch for errors [5]. Underneath it's still promises; the sugar just flattens it.

```
// same operation, two surfaces
fetch('/api/likes').then(r => r.json()).then(data => render(data));

async function loadLikes() {
  const r = await fetch('/api/likes');
  const data = await r.json();
  render(data);
}
```

The second reads like synchronous code, but every await is a yield point — control returns to the event loop, other handlers run, and when the awaited promise settles, this function resumes. That's the trick that keeps a slow network request from locking the page.

## The event loop — the one picture that made it click

The payoff of all the above is one diagram. The event loop is the scheduler that ties values, the DOM, events, and async together — and seeing it as a picture is what made async stop feeling like magic:

```figure
<svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The JavaScript event loop. Left box: Call Stack — the single thread running one function at a time. Top right box: Web APIs — work handed off to the browser (timers, network fetch), runs outside JS. Bottom right box: Callback / Microtask Queue — functions waiting to run once the stack is empty. A circular arrow labelled event loop cycles: when the stack is empty, it pulls the next task from the queue and pushes it onto the stack.">
  <defs>
    <marker id="jsarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Call stack -->
    <rect x="40" y="90" width="180" height="140" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="130" y="116" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Call Stack</text>
    <text x="130" y="134" font-size="10.5" fill="#475569" text-anchor="middle">one function at a time</text>
    <rect x="60" y="150" width="140" height="22" rx="4" fill="#c7d2fe"/>
    <text x="130" y="165" font-size="10" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">handleClick()</text>
    <rect x="60" y="176" width="140" height="22" rx="4" fill="#c7d2fe"/>
    <text x="130" y="191" font-size="10" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">render()</text>

    <!-- Web APIs -->
    <rect x="460" y="50" width="220" height="80" rx="10" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="570" y="76" font-size="13" font-weight="700" fill="#500724" text-anchor="middle">Web APIs (browser)</text>
    <text x="570" y="94" font-size="10.5" fill="#500724" text-anchor="middle">timers · fetch · DOM events</text>
    <text x="570" y="112" font-size="10" fill="#475569" text-anchor="middle">runs outside JS — no blocking</text>

    <!-- Task queue -->
    <rect x="460" y="180" width="220" height="80" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="570" y="206" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">Callback Queue</text>
    <text x="570" y="224" font-size="10.5" fill="#052e16" text-anchor="middle">waiting functions, in order</text>
    <text x="570" y="242" font-size="10" fill="#475569" text-anchor="middle">promises + event handlers</text>

    <!-- handoff: stack -> web api -->
    <path d="M220,120 C330,100 380,90 458,90" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#jsarrow)"/>
    <text x="339" y="84" font-size="10" fill="#475569" text-anchor="middle">hand off slow work</text>

    <!-- settle: web api -> queue -->
    <path d="M570,130 L570,178" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#jsarrow)"/>
    <text x="612" y="158" font-size="10" fill="#475569" text-anchor="middle">when done, queue its callback</text>

    <!-- loop: queue -> stack -->
    <path d="M458,220 C330,200 280,180 222,160" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#jsarrow)"/>
    <text x="330" y="232" font-size="10" fill="#475569" text-anchor="middle">event loop: pull next when stack empty</text>
  </g>
</svg>
```

The loop's rule is dead simple: **if the call stack is empty, take the next callback from the queue and run it.** [2] That's why setTimeout(fn, 0) doesn't run fn immediately — it queues it, and the loop only picks it up once the current stack finishes. Once this picture is in your head, a lot of "why did this run in the wrong order?" bugs just dissolve: the answer is always "because the stack wasn't empty yet."

## How I use this

The habit these notes left me with is one question whenever a UI feels janky or a callback fires late: **what's on the stack, and what's in the queue?** A slow for loop is on the stack and blocks everything; a fetch is off the stack and in the web-API lane, freeing the loop to keep the page responsive. Reaching for the async, non-blocking version first — fetch over a giant synchronous parse, await over a manual callback chain — is the practical shape of "don't block the single thread."

That's also why the behavior layer felt arbitrary before I had the model. I was treating JS as "HTML plus some snippets," and async in particular read like a pile of special cases. Seen as one thread clearing a stack on an event loop, with promises and await as the syntactic shape of yielding to that loop — it's one idea, consistently applied.

## References

[1] I. Kantor and the javascript.info team, "The Modern JavaScript Tutorial," javascript.info, 2024. [Online]. Available: [https://javascript.info/](https://javascript.info/)

[2] Mozilla, "Concurrency model and the event loop," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)

[3] Mozilla, "Introduction to the DOM," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction)

[4] Mozilla, "Introduction to events," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events)

[5] Mozilla, "Using promises," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)

[6] W. Bos, "JavaScript30 — Build 30 things in 30 days with vanilla JS," javascript30.com, 2024. [Online]. Available: [https://javascript30.com/](https://javascript30.com/)

```quiz
Q: In the three-layer way of thinking, what is JavaScript's job on a web page?
- Meaning — what each piece of content is
- Looks — how the content appears
- Behavior — what the page does, how it reacts
correct: 2
explain: HTML is meaning, CSS is looks, JS is behavior. JavaScript reads and mutates the DOM in response to events — that's what makes a static page reactive.

Q: Why does a slow synchronous loop freeze the whole page?
- Because the browser is single-process
- Because JS runs on a single thread with one call stack, and the loop can't pull the next task until the stack clears
correct: 1
explain: JS is single-threaded. A long-running function sits on the call stack and blocks the event loop from handling anything else — clicks, timers, scrolls all queue up behind it.

Q: `async`/`await` is best described as…
- a completely new concurrency mechanism separate from promises
- syntactic sugar over promises that lets async code read top-to-bottom, with try/catch
correct: 1
explain: Underneath, `async`/`await` is still promises. Every `await` is a yield point — control returns to the event loop, and the function resumes when the awaited promise settles.

Q: The event loop's rule for when to run the next queued callback is…
- whenever a callback is added to the queue, interrupt the current code
- only when the call stack is empty, pull the next callback and run it
correct: 1
explain: The loop checks: is the stack empty? If yes, take the next task from the queue. That's why `setTimeout(fn, 0)` doesn't fire immediately — it waits for the current stack to finish.

Q: You mutate an object inside a function and the caller sees the change. Why?
- Because objects are passed by value
- Because objects are passed by reference — the variable holds a reference to the same object
correct: 1
explain: Primitives are passed by value; objects (including arrays and functions) are passed by reference. Mutating the object inside the function edits the shared thing the caller also holds.
```
