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 thetypefield in the nearest package manifest; a CommonJS file is wrapped and receives the namesrequire,module,__filenameas 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 theircode,syscall, andpathfields.
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.