Skip to main content

βš™οΈ JavaScript Execution Environment

The same JavaScript file behaves differently depending on where it runs. This lesson maps the two homes JavaScript lives in β€” the browser and Node.js β€” and then goes under the hood to the engine, the call stack, and the event loop that let a single-threaded language stay responsive.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define a JavaScript execution environment and name the capabilities each one provides
  • Contrast the browser environment (DOM, window, Web APIs) with Node.js (file system, global, modules)
  • Explain how the engine, call stack, and memory heap execute your code
  • Trace how the event loop and callback/microtask queues handle asynchronous work
  • Write environment-aware code that detects where it is running

Estimated Time: 30–40 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Predict the output order of a mixed synchronous / async snippet, then verify it.

In This Lesson

What Is an Execution Environment?

An execution environment is the "world" your JavaScript lives in. The core language β€” variables, functions, loops, objects β€” is defined by ECMAScript and is identical everywhere. But the language on its own can't open a file, draw on the screen, or make a network request. Those powers come from the environment wrapped around the engine.

πŸ’‘ Same skills, different fields. A tennis player uses the same strokes on clay, grass, and hard courts, but adapts to each surface. JavaScript keeps its syntax everywhere, but the browser and Node.js give it very different surfaces to play on.

The two environments you'll meet constantly are the browser and Node.js. Each hands JavaScript a different toolbox:

graph TD A[JavaScript Execution Environments] A --> B[Browser] A --> C[Node.js] B --> D[DOM API] B --> E[window object] B --> F[fetch, storage, geolocation] C --> H[File system] C --> I[HTTP servers] C --> J[process object] C --> K[npm modules]

The Browser Environment

The browser is JavaScript's original home. Here the global object is window, and the browser exposes a rich set of Web APIs for interacting with the page and the device.

  • window β€” the global object representing the browser tab; global variables and functions hang off it.
  • DOM (Document Object Model) β€” a live, tree-shaped representation of the HTML that JavaScript can read and change.
  • Web APIs β€” fetch for network requests, localStorage for storage, geolocation, canvas, audio, notifications, and more.
// In the browser, the global object is window
window.location.href;          // the current URL
window.localStorage;           // key/value storage API

// Feature-detect before using an API
if ('localStorage' in window) {
  localStorage.setItem('theme', 'dark');
} else {
  console.log('This browser has no localStorage');
}

The DOM as a tree

The DOM turns your HTML into a tree of nodes. JavaScript walks and edits that tree to build interactive pages.

graph TD A[document] --> B[html] B --> C[head] B --> D[body] C --> E[title] D --> F[header] D --> G[main] G --> H[section] H --> I[p]
// Read and change the DOM
const heading = document.querySelector('h1');
heading.textContent = 'Updated by JavaScript';

// Create and insert a new element
const note = document.createElement('p');
note.textContent = 'Added at runtime';
document.body.appendChild(note);

// React to user input
document.querySelector('button')?.addEventListener('click', () => {
  console.log('Button clicked!');
});

πŸ“– The DOM as a family tree

Think of document as the ancestor and every element as a descendant. We navigate by relationships β€” parent, child, sibling β€” and "editing the DOM" is like adding, renaming, or pruning branches of that family tree.

The Node.js Environment

Node.js (Ryan Dahl, 2009) took Chrome's V8 engine out of the browser and paired it with system-level APIs, so JavaScript could run on servers and command-line tools. There is no window and no DOM here β€” instead you get access to the machine.

  • global β€” the global object, in place of window.
  • fs β€” read and write files.
  • http / https β€” create web servers and clients.
  • process β€” details about the running process (version, platform, environment variables).
  • npm β€” a registry of millions of installable packages.

A minimal web server in Node.js:

// server.js β€” a bare-bones HTTP server
const http = require('http');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});
πŸ’‘ Tour guide vs. general contractor. Browser JavaScript is like a tour guide who knows one museum (the page) inside out and helps visitors interact with it. Node.js JavaScript is like a general contractor who can build whole structures and manage utilities. Same language, very different jobs.

Browser vs. Node.js

The language is the same; the surroundings differ. This table captures the differences you'll hit most often.

FeatureBrowserNode.js
Global objectwindowglobal (or globalThis)
DOM APIAvailableNot available
File systemVery limited (File API)Full access (fs module)
Network requestsfetchfetch, http/https
Module systemES Modules (import/export)CommonJS (require) & ES Modules
Security modelSandboxed, many restrictionsFull system access (within user permissions)

globalThis is worth remembering: it's the standardized name for the global object that works in every environment, so you no longer have to guess between window and global. (And note that fetch is now built into modern Node.js too, so that gap has narrowed.)

Writing environment-aware code

Some code runs in both places β€” this is called isomorphic or universal JavaScript, and frameworks like Next.js and Nuxt rely on it. You branch on what's available:

async function loadProduct(id) {
  if (typeof window === 'undefined') {
    // Server (Node.js): talk to the database directly
    const db = require('./database');
    return db.query('SELECT * FROM products WHERE id = ?', [id]);
  } else {
    // Client (browser): call our own API
    const response = await fetch(`/api/products/${id}`);
    return response.json();
  }
}

⚠️ A common beginner error

Calling document.getElementById(...) in Node.js throws ReferenceError: document is not defined, and calling require('fs') in the browser fails too. When code has to run in both places, guard environment-specific calls behind a check like the one above.

Inside the Runtime

Whichever environment you're in, a JavaScript engine does the actual work of parsing and running your code. The famous ones are V8 (Chrome, Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari).

Components of a JavaScript runtime The engine contains a memory heap and a call stack. Around it, the environment provides Web or system APIs, a callback queue, a microtask queue, and an event loop that feeds ready callbacks back into the call stack. Runtime (browser or Node.js) JavaScript Engine (e.g. V8) Memory Heap objects live here Call Stack one frame at a time Web / System APIs fetch, timers, fs, DOM events Microtask Queue promises Callback Queue timers, events Event Loop stack empty? β†’ feed next callback in
Figure 1 β€” The engine (heap + call stack) runs your code. The surrounding environment provides APIs and queues, and the event loop decides when queued callbacks return to the stack.

The call stack

JavaScript is single-threaded: it does exactly one thing at a time, tracked by the call stack. Calling a function pushes a frame on top; returning pops it off. The last function pushed is the first to finish β€” last in, first out, like a stack of plates.

function first() {
  console.log('first: start');
  second();
  console.log('first: end');
}
function second() {
  console.log('second: start');
  third();
  console.log('second: end');
}
function third() {
  console.log('third');
}

first();
// first: start
// second: start
// third
// second: end
// first: end

The Event Loop

If JavaScript can only do one thing at a time, how does it fetch data or wait for a timer without freezing? The answer is the event loop. Slow operations are handed off to the environment's APIs; when they finish, their callbacks wait in a queue. The event loop moves a waiting callback onto the call stack only once the stack is empty.

console.log('First');

setTimeout(() => {
  console.log('Second β€” after the delay');
}, 2000);

console.log('Third');

// Output:
// First
// Third
// Second β€” after the delay
sequenceDiagram participant CS as Call Stack participant WA as Web API participant CB as Callback Queue participant EL as Event Loop Note over CS: console.log("First") CS->>WA: setTimeout(cb, 2000) Note over CS: console.log("Third") WA->>CB: after 2s, queue cb EL->>CS: stack empty, run cb Note over CS: console.log("Second")

Microtasks jump the line

There are actually two queues. Promise callbacks go on the microtask queue, and the event loop empties all microtasks before touching the ordinary callback (macrotask) queue used by timers. That's why promises resolve ahead of a setTimeout(..., 0):

console.log('Start');

setTimeout(() => console.log('Timeout'), 0); // macrotask

Promise.resolve().then(() => console.log('Promise')); // microtask

console.log('End');

// Output:
// Start
// End
// Promise   ← microtasks run before timers
// Timeout

βœ… Why this matters

Without the event loop, one slow network call would freeze the entire page β€” no scrolling, no clicks. With it, JavaScript delegates the wait, keeps the interface responsive, and runs your callback the moment the result is ready. This model is the beating heart of every interactive web app.

Hands-on Exercise

πŸ‹οΈ Predict the Event Loop

Objective: Reason about ordering across synchronous code, microtasks, and macrotasks β€” then confirm in a real console.

The code:

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'));

console.log('5');

Your task:

  1. Write down the order you expect the numbers to print, before running anything.
  2. Open your browser console (or a Node.js REPL), paste the code, and run it.
  3. Explain any surprise using the two-queue model from this lesson.
πŸ’‘ Hint

First, all the synchronous lines run top to bottom. Then the microtask queue (the .then callbacks) drains completely. Only after that does the event loop reach the setTimeout macrotask.

βœ… Solution

The output is:

1
5
3
4
2

Why: 1 and 5 are synchronous, so they print first in order. The two .then callbacks are microtasks, so 3 then 4 print before any timer. The setTimeout callback is a macrotask and runs last, printing 2 β€” even though its delay was 0.

🎯 Quick Quiz

Question 1: Which capability exists in the browser environment but not in Node.js?

Question 2: JavaScript is single-threaded. What lets it handle asynchronous work without blocking?

Question 3: In the event loop, when do promise callbacks (microtasks) run relative to setTimeout callbacks?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • An execution environment wraps the engine with the APIs your code actually needs β€” the language alone can't touch files, screens, or networks.
  • The browser gives you window, the DOM, and Web APIs; Node.js gives you global, the file system, and servers.
  • The engine (V8, SpiderMonkey, JavaScriptCore) runs code with a memory heap and a single call stack.
  • The event loop lets single-threaded JavaScript stay responsive by queueing callbacks and running them when the stack is clear.
  • Microtasks (promises) run before macrotasks (timers) β€” a detail that explains a lot of "surprising" output order.

πŸ“š Further Reading

πŸš€ What's Next?

You now know where JavaScript runs and how it schedules work. Next you'll get hands-on with the single most useful tool for exploring all of this live β€” the browser console.

πŸŽ‰ Great progress!

The runtime is no longer a black box. Time to open the console and start poking at it.