Skip to content
academia.sh

Lesson 01 / 20

Differences from the Browser

The host environment concept, the global object's contents, the two module systems coexisting, and the server runtime's capability boundaries.

Contents

The Asynchronous JavaScript and the Runtime course established the single-threaded execution model and the event loop. Everything covered there belonged not to the language but to the environment hosting it: the task queue and the timers are not defined in the language standard either. This course takes up the same language in a different host environment.

The question: when the same syntax, value model, and event-loop concept run in a server process instead of a browser, what changes? The answer falls under two headings — which names are available, and what the process is authorized to do.

The Language Core and the Host Environment

The language standard defines syntax and the value model: Array, Map, Promise, JSON, Math belong to this layer. It says how these values behave, not how to reach the outside world with them.

The host environment is the program running the language, and it provides the contact with the outside world. A browser is a host environment: it offers the document tree, cookies, screen events. A server runtime is a host environment too: it offers the file system, process information, network listening.

The distinction can be tested directly. The file below prints whether a set of names is present on the global object:

// environment.mjs
const names = [
  'window', 'document', 'localStorage', 'alert',
  'process', 'Buffer', 'global',
  'fetch', 'URL', 'TextEncoder', 'AbortController', 'structuredClone',
];
for (const name of names) {
  console.log(`${name.padEnd(16)} ${name in globalThis ? 'yes' : 'no'}`);
}
node environment.mjs
window           no
document         no
localStorage     no
alert            no
process          yes
Buffer           yes
global           yes
fetch            yes
URL              yes
TextEncoder      yes
AbortController  yes
structuredClone  yes

The output shows three groups. The first exists only in the browser: the document tree, page storage, and UI calls have no counterpart on the server. The second exists only server-side: process represents process information, Buffer represents raw bytes.

The third group is more interesting. fetch, URL, TextEncoder, AbortController, and structuredClone are browser interfaces, but they belong to the web platform, not the language. The server runtime implements a portion of these under the same contract, so code running in both environments does not need two separate versions of every interface. This overlap keeps growing: if an interface is documented as present from a given version onward, it can be relied on; where it might be missing, a presence check is used.

The name global is an older name pointing to the same object as globalThis. Writing globalThis in new code makes the same expression work in both environments.

Two Module Systems

The browser has a single module form. The server runtime carries two forms together and picks one per file.

CommonJS loads synchronously through a require call and exports by writing to a module.exports object. ES modules work through import and export declarations and resolve dependencies before running them. The difference between the two was detailed in the Modules, Tooling and the Ecosystem course; what matters here is how the choice is made.

The choice looks at two rules: file extension and the nearest package manifest. A .cjs file is always CommonJS, .mjs is always an ES module. For a .js extension, the decision comes from the type field in the first package.json found walking up from the file’s directory: module means an ES module, commonjs or an absent field means CommonJS.

The runtime difference between the two forms is measurable:

// scope.cjs
console.log('this === module.exports :', this === module.exports);
console.log('__filename last part   :', __filename.split('/').pop());
console.log('typeof require         :', typeof require);
// scope.mjs
console.log('this                    :', this);
console.log('import.meta.url last part:', import.meta.url.split('/').pop());
console.log('typeof require          :', typeof require);
node scope.cjs
this === module.exports : true
__filename last part   : scope.cjs
typeof require         : function
node scope.mjs
this                    : undefined
import.meta.url last part: scope.mjs
typeof require          : undefined

A CommonJS file is wrapped like a function body: require, module, __filename, and __dirname are that wrapper’s parameters, not globals. An ES module has none of these; its own location is given through import.meta.url, not as a file path but as an address. ES modules are used throughout the course; CommonJS appears only where the difference needs to be seen.

Capability and Authority

The real difference is not in the name list, it is in what the process can do.

Code running in a browser is treated as hostile toward the user’s machine and kept in a sandbox: it cannot read an arbitrary file, cannot listen on an arbitrary port, and can only reach another origin to the extent that origin permits. These restrictions are the guarantees a browser gives the user.

The server runtime has no such sandbox. The process runs with the authority of the user who started it. The permission model from the Introduction to Linux course applies here directly: whatever files the process can read, your script can read too. The same holds for writing and deleting.

This authority shows in the short file below:

// first-read.mjs
import { readFileSync } from 'node:fs';

const text = readFileSync('measurements.ndjson', 'utf8');
const lines = text.trim().split('\n');

console.log('line count:', lines.length);
console.log('first record:', JSON.parse(lines[0]));

The node: prefix declares that the name points to a built-in module. It resolves without the prefix too; the prefixed spelling settles which one is meant if a file or package under the same name exists. The course uses the prefixed form throughout.

The Course’s Working File

One concrete piece of work will be built throughout this course: a small service that collects records from remote measurement nodes, summarizes them, and serves them over HTTP. The input is a text file where every line is an independent JSON object. Save the twelve lines below under the name measurements.ndjson:

{"time":"2024-02-07T09:12:44Z","node":"edge-01","metric":"temperature","value":21.4}
{"time":"2024-02-07T09:12:44Z","node":"edge-01","metric":"humidity","value":48.0}
{"time":"2024-02-07T09:13:02Z","node":"edge-02","metric":"temperature","value":19.8}
{"time":"2024-02-07T09:13:02Z","node":"edge-02","metric":"humidity","value":52.5}
{"time":"2024-02-07T09:14:19Z","node":"edge-01","metric":"temperature","value":21.9}
{"time":"2024-02-07T09:14:19Z","node":"edge-03","metric":"temperature","value":24.1}
{"time":"2024-02-07T09:15:30Z","node":"edge-02","metric":"temperature","value":20.2}
{"time":"2024-02-07T09:15:30Z","node":"edge-03","metric":"humidity","value":41.3}
{"time":"2024-02-07T09:16:04Z","node":"edge-01","metric":"humidity","value":47.2}
{"time":"2024-02-07T09:16:04Z","node":"edge-03","metric":"temperature","value":24.6}
{"time":"2024-02-07T09:17:22Z","node":"edge-02","metric":"temperature","value":20.7}
{"time":"2024-02-07T09:17:22Z","node":"edge-01","metric":"temperature","value":22.3}

The four fields on each line carry, in order, the moment of measurement, the sending node, the measured quantity, and the value. This one-record-per-line format is compatible with the line-based filters from the Shell Programming course: it can be processed line by line without loading the whole file into memory — decisive in the Streams topic.

After putting the file in place, run the first read:

node first-read.mjs
line count: 12
first record: {
  time: '2024-02-07T09:12:44Z',
  node: 'edge-01',
  metric: 'temperature',
  value: 21.4
}

This shows work with no browser counterpart: a local file was read by name, without user consent. The rest of the course builds on this capability — and takes up the responsibility it brings.

The error that comes up when a file is not found is a form you will see often throughout the course:

node -e "import('node:fs').then(m => m.readFileSync('missing.ndjson','utf8')).catch(e => console.log(e.code, '|', e.syscall, '|', e.path))"
ENOENT | open | missing.ndjson

ENOENT is the error name the operating system returns from the system call; syscall says which call failed, path says for which path. These three fields let you decide without reading the message text — the text can change, the code does not.

Summary

  • The language standard defines the value model; access to the outside world is the host environment’s responsibility. The browser and the server runtime are two separate host environments.
  • The global object has three groups: names found only in the browser, names found only on the server, and shared interfaces inherited from the web platform.
  • Module form is chosen by file extension (.cjs, .mjs) or the type field in the nearest package manifest; a CommonJS file is wrapped and receives the names require, module, __filename as parameters.
  • A server process has no sandbox; the process runs with the file and network authority of the user who started it.
  • Writing built-in modules with the node: prefix removes the chance of a name collision; file system errors are distinguished by their code, syscall, and path fields.

Next Step

In this lesson, the file was read with readFileSync — by making the process wait. In a single-threaded server, that waiting is costly: no other request can be processed during it. The next lesson opens up, phase by phase, the mechanism used to avoid that cost — with measurable examples of the order the event loop drains its queues, when timers run, and where input/output callbacks enter that order.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close