JavaScript (ES2026) Module 1 – Start here: what JavaScript is and how to run it
Run JavaScript in the browser, the runner and Node
Run JavaScript three ways: the Run buttons of this track, the browser console and Node.js 24, with the REPL, --watch and how Node picks a module type.
What you will learn
- Run a script with a Run button on these pages, in the browser console and with Node.js 24
- Install a pinned Node.js version with mise or the official installer and check it with node --version
- Predict whether Node.js runs a file as an ES module or as CommonJS
- Use the REPL, node -e, node -p and node --watch for quick experiments
Before you start
What you’ll learn · Before you start
What you will learn
- Run a script with a Run button on these pages, in the browser console and with Node.js 24
- Install a pinned Node.js version with mise or the official installer and check it with node --version
- Predict whether Node.js runs a file as an ES module or as CommonJS
- Use the REPL, node -e, node -p and node --watch for quick experiments
Before you start
On this page
You can run JavaScript in three places, and this track uses all of them. The Run buttons on these pages run an
example inside your browser in a moment, with nothing to install. The browser console, part of the developer tools
of every desktop browser, runs code next to a real web page. Node.js, installed on your computer, runs files from a
terminal, and every output shown in this track comes from it, from Node.js 24.21.0. The three share the language
but not the host, as the first lesson showed, so the same program can print different things in each. This lesson
runs small programs in all three, installs a pinned version of Node.js, and covers the tools you will use for quick
experiments: the REPL, node -e, node -p and node --watch.
The Run buttons on these pages
Examples marked with a Run bar can run in your browser. Press Run and the page starts the file in QuickJS, a small JavaScript engine compiled to WebAssembly (quickjs-emscripten 0.32.0), inside a Web Worker, so a long loop cannot freeze the page. The first run downloads the engine, about 0.6 MB, and keeps it for later runs; your code runs on your own device. Edit opens the code so you can change it and run your version, and Reset brings back the original.
// Says which host runs it: Node.js reports its own version in process.versions.node.
const node = globalThis.process?.versions?.node;
console.log(node ? `Hello from Node.js ${node}` : 'Hello from an engine without Node.js'); Output
Hello from Node.js 24.21.0
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node hello.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
In the Run button’s QuickJS there is no process object, so globalThis.process?.versions?.node is undefined and
the program takes its second branch. The output above is what Node.js 24.21.0 printed.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
Hello from an engine without Node.js
The Run button gives QuickJS a deliberately small host: console, setTimeout, setInterval and queueMicrotask.
A file whose name ends in .mjs runs as a module there, so it may import other files of the same example. There is
no fetch, no process and no page to change, so examples that need those show what Node.js recorded and have no Run
bar. Timers behave in one more way that you should know about before you trust a timing:
// Two timers and a line of code after them: in which order do the three lines print?
const started = Date.now();
setTimeout(() => {
const waited = Date.now() - started;
console.log(`the 50 ms timer, after a real wait of at least 50 ms: ${waited >= 50}`);
}, 50);
setTimeout(() => console.log('the 10 ms timer'), 10);
console.log('the line after both setTimeout calls'); Output
the line after both setTimeout calls the 10 ms timer the 50 ms timer, after a real wait of at least 50 ms: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node timers.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
On Node.js the line after both calls comes first, because a timer’s callback runs only after the code that set it has finished. Then the 10 ms timer fires, then the 50 ms one, after a real wait of 50 ms.
Version note
The Run button keeps the same order but does not wait. Its timers run on a virtual clock that jumps from one timer to
the next, so the 50 ms timer fires almost at once and the program prints false for the real wait. Use Node.js or
the browser console for anything that measures time.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
the line after both setTimeout calls the 10 ms timer the 50 ms timer, after a real wait of at least 50 ms: false
The browser console
Every desktop browser has developer tools with a console: a prompt where each line you type runs at once, in the page
that is open. Its host is the full browser host, with document, fetch and localStorage, and its engine is the
browser’s own: V8 in Chrome, SpiderMonkey in Firefox, JavaScriptCore in Safari. To open it:
| Browser | Open the console |
|---|---|
| Chrome | Ctrl+Shift+J on Windows and Linux, Cmd+Option+J on a Mac |
| Firefox | Ctrl+Shift+K, or Cmd+Option+K on a Mac |
| Safari | First turn on Settings › Advanced › “Show features for web developers”; then Develop › Show Web Inspector (Option+Cmd+I), whose console prompt is at the bottom |
Code you type in the console runs inside the page that is open, with that page’s variables, so a blank tab is the
cleanest place for experiments. Chrome adds a safety step: if you are new to its developer tools and paste code into
the console, it warns you about scams that ask people to paste code, and you have to type allow pasting once before
pasting works. The warning is worth taking seriously. Code pasted into a page’s console runs with your login on that
site, so paste only code that you have read and understood.
For a page of your own, the HTML, CSS & JS Playground runs your script inside a real
page. The JavaScript Online Compiler runs it in a Web Worker instead, with your
browser’s own engine; it runs your code as a classic script, so import and export statements do not work there.
Node.js on your computer
Node.js runs JavaScript files from a terminal. Install it with the official installer or prebuilt binaries from nodejs.org, or with a version manager. This track uses mise, which installs exact versions side by side and picks the right one per project:
mise use node@24.21.0installs that version if needed and records it in the project’smise.toml, so everyone who works on the project gets the same Node.js.mise exec node@24.21.0 -- node hello.mjsruns one command with exactly that version, whatever is installed elsewhere.
Either way, check the result with node --version. The Node.js project advises that production applications use
only LTS release lines, and Node.js 24 is one. Pin the version per project rather than relying on whatever node a
shell finds first. A terminal where your version manager is active can find another node than a script started by a
scheduler, an editor or a CI job, where the version manager is not active: say Node.js 24 from mise in your terminal
and an older Node.js from the operating system’s packages in a scheduled job. Recorded outputs in this track always
name the exact command, mise exec node@24.21.0 -- node …, so you can repeat them with the same version.
The session below shows the commands this lesson uses. node -e evaluates the code you give it, node -p evaluates
it and prints the result, and node <file> runs a file. Words typed after the file name arrive in process.argv,
after two entries that Node.js puts first, its own path and the file’s path:
// node greet.mjs Asha Ravi: the words after the file name arrive in process.argv,
// after two entries that Node.js puts first (its own path and the path of this file).
import { argv } from 'node:process';
const names = argv.slice(2);
console.log(`process.argv has ${argv.length} entries, ${names.length} of them typed after the file name`);
for (const name of names) console.log(`Namaste, ${name}!`); import { run } from './session.mjs';
// Which Node.js is this, a one-line program with -e, an expression with -p, and two files.
run([
'node --version',
'node -e "console.log([1, 2, 3].map((n) => n * 2))"',
'node -p "2 ** 10"',
'node hello.mjs',
'node greet.mjs Asha Ravi',
]); Output
$ node --version v24.21.0 $ node -e "console.log([1, 2, 3].map((n) => n * 2))" [ 2, 4, 6 ] $ node -p "2 ** 10" 1024 $ node hello.mjs Hello from Node.js 24.21.0 $ node greet.mjs Asha Ravi process.argv has 4 entries, 2 of them typed after the file name Namaste, Asha! Namaste, Ravi!
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node node-basics.mjs
The session script that typed these commands is a short helper, shown here for completeness. It prints each command
after $ , then its output, as a terminal does:
// Runs shell commands the way a terminal shows them and prints the session: each command after "$ ", then what it
// printed (standard output and error together). A command that fails is followed by "[exit status N]"; unless it is
// listed in expectFailure, the session then fails too. `node` is the Node.js that runs this file. Two details change
// from run to run and are written in a fixed form: the folder the session runs in is shown as ".", and the process
// id that Node.js puts in warnings, "(node:12345)", as "(node:PID)". Output stops at the first stack frame (" at …").
import { spawnSync } from 'node:child_process';
import { dirname } from 'node:path';
const folder = process.cwd();
const env = { ...process.env, PATH: `${dirname(process.execPath)}:${process.env.PATH ?? ''}` };
function tidy(text) {
const lines = text.replaceAll(folder, '.').replace(/\(node:\d+\)/g, '(node:PID)').split('\n');
const frame = lines.findIndex((line) => line.startsWith(' at '));
return (frame === -1 ? lines : lines.slice(0, frame)).join('\n');
}
export function run(commands, { expectFailure = [] } = {}) {
let unexpected = 0;
for (const command of commands) {
console.log(`$ ${command}`);
const done = spawnSync('/bin/sh', ['-c', `${command} 2>&1`], { env, encoding: 'utf8' });
const shown = tidy(done.stdout).trimEnd();
if (shown) console.log(shown);
if (done.status !== 0) {
console.log(`[exit status ${done.status}]`);
if (!expectFailure.includes(command)) unexpected += 1;
}
}
if (unexpected) process.exitCode = 1;
} ES module or CommonJS?
Node.js has two module systems: ES modules, the standard one with import and export, and CommonJS, its
older system with require(). It decides for each file which one to use, and the choice changes what works inside the
file:
| File | How Node.js 24 runs it |
|---|---|
name.mjs |
Always as an ES module |
name.cjs |
Always as CommonJS |
name.js, nearest package.json has "type": "module" |
As an ES module |
name.js, nearest package.json has "type": "commonjs" |
As CommonJS |
name.js with no type field or no package.json |
As CommonJS, unless the code uses ES module syntax such as import; then as an ES module |
The last row is called syntax detection, and it is on by default in Node.js 24. It costs a second parse, and with a
package.json that lacks a type field Node.js prints a warning about it, so a project should always state its type.
Three small files show the rules at work, in a folder with no package.json:
detect.js
import { argv } from 'node:process';
console.log('detect.js ran as', typeof require === 'undefined' ? 'an ES module' : 'CommonJS', 'with', argv.length, 'arguments'); plain.js
console.log('plain.js ran as', typeof require === 'undefined' ? 'an ES module' : 'CommonJS'); detect.cjs
import { argv } from 'node:process';
console.log('detect.cjs ran with', argv.length, 'arguments'); import { run } from './session.mjs';
// Three files, one question: does Node.js run each one as an ES module or as CommonJS?
// There is no package.json in this folder, so only the extension and the code itself decide.
run(['node detect.js', 'node plain.js', 'node detect.cjs'], { expectFailure: ['node detect.cjs'] }); Output
$ node detect.js
detect.js ran as an ES module with 2 arguments
$ node plain.js
plain.js ran as CommonJS
$ node detect.cjs
(node:PID) Warning: Failed to load the ES module: ./detect.cjs. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
./detect.cjs:1
import { argv } from 'node:process';
^^^^^^
SyntaxError: Cannot use import statement outside a module
[exit status 1]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node module-kinds.mjs
detect.js contains an import statement, so syntax detection ran it as an ES module, where require does not
exist. plain.js has no module syntax and ran as CommonJS. detect.cjs failed with
SyntaxError: Cannot use import statement outside a module: the .cjs extension makes it CommonJS, and the warning
above the error suggests both fixes. Copy plain.js into a project whose package.json says "type": "module" and it
runs as an ES module instead, which is why the same file can behave differently in two folders. The examples of this
track are .mjs files, so they are ES modules wherever you save them.
The REPL for quick checks
Run node with no file and you get the REPL (read, evaluate, print, loop): a > prompt that evaluates each line and
prints its value. Three of its features save time. _ holds the value of the last expression; await works at the
top level, without an async function around it; and .help lists the commands, among them .editor for typing
several lines at once and .exit to quit, which Ctrl+D does too.
// Types five lines into the Node.js REPL (node -i) and prints the session as a terminal shows it. Standard input is
// not a terminal here, so the REPL does not echo what was typed: the script prints each typed line after its prompt.
import { spawnSync } from 'node:child_process';
const typed = ['2 ** 10', '[3, 1, 2].toSorted()', '_.length', 'await Promise.resolve("top-level await works")', '.exit'];
const repl = spawnSync(process.execPath, ['-i'], { input: `${typed.join('\n')}\n`, encoding: 'utf8' });
// The REPL prints a prompt, "> ", before each line it reads, and the result of that line after it.
const [banner, ...results] = repl.stdout.split('> ');
process.stdout.write(banner);
typed.forEach((line, i) => {
console.log(`> ${line}`);
const result = (results[i] ?? '').trimEnd();
if (result) console.log(result);
}); Output
Welcome to Node.js v24.21.0.
Type ".help" for more information.
> 2 ** 10
1024
> [3, 1, 2].toSorted()
[ 1, 2, 3 ]
> _.length
3
> await Promise.resolve("top-level await works")
'top-level await works'
> .exit
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node repl-session.mjs
The REPL is for trying an idea. Anything you want to keep or run again belongs in a file.
Re-running a file on every save
node --watch app.mjs runs the file and then waits; whenever you save the file, or a module it imports, Node.js
restarts it. Watch mode has been stable since Node.js 22. In a terminal it clears the screen before each restart
(--watch-preserve-output keeps the old output); this session sends the output to a script instead, so every line
stays:
// Starts `node --watch app.mjs`, edits app.mjs once, and prints what the watcher printed.
// The folder name in "Change detected in …" is shortened to ".", because it differs on every computer.
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';
writeFileSync('app.mjs', "console.log('version 1 of app.mjs');\n");
const watcher = spawn(process.execPath, ['--watch', 'app.mjs'], { stdio: ['ignore', 'pipe', 'pipe'] });
let printed = '';
watcher.stdout.on('data', (chunk) => (printed += chunk));
watcher.stderr.on('data', (chunk) => (printed += chunk));
// Waits until `text` has been printed `times` times (at most five seconds).
async function waitFor(text, times = 1) {
for (let i = 0; i < 200 && printed.split(text).length - 1 < times; i++) await sleep(25);
}
await waitFor('Waiting for file changes');
console.log('$ node --watch app.mjs (then app.mjs is edited and saved)');
writeFileSync('app.mjs', "console.log('version 2 of app.mjs');\n");
await waitFor('Completed running', 2);
watcher.kill('SIGTERM');
await new Promise((done) => watcher.on('exit', done));
process.stdout.write(printed.replaceAll(process.cwd(), '.')); Output
$ node --watch app.mjs (then app.mjs is edited and saved) version 1 of app.mjs Completed running 'app.mjs'. Waiting for file changes before restarting... Change detected in './app.mjs' Restarting 'app.mjs' version 2 of app.mjs Completed running 'app.mjs'. Waiting for file changes before restarting...
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node watch-session.mjs
Exercise: greet the host
Write greeting(g), which looks at a global object and names the host it belongs to: Node.js with its version, a
browser, or an unknown host. The sample tests run in the Run button’s QuickJS, and they hand your function small
objects that pretend to be one host or another, so the same tests pass in every engine.
Exercise · Easy · JavaScript
Greet the host your code runs in
greeting(g) receives a global object and returns one line that names the host it belongs to:
"Hello from Node.js 24.21.0"wheng.process.versions.nodeis a string, here"24.21.0"(use the version it finds, so Node.js 26.10.0 gives"Hello from Node.js 26.10.0");"Hello from a browser"when there is no Node.js version butg.documentexists;"Hello from an unknown host"in every other case, including aprocessobject withoutversions.
Without an argument, greeting() looks at the real global object, globalThis. The tests pass small objects that pretend to be one host or another, so your function must read only the g it is given, and must never throw: a missing property means "not this host".
Starter code · greeting.mjs
/**
* One line that names the host of the global object `g`: Node.js with its version, a browser, or an unknown host.
* @param {object} [g] a global object; the real one by default
* @returns {string}
*/
export function greeting(g = globalThis) {
return '';
} The sample tests · greeting.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { greeting } from './greeting.mjs';
test('names Node.js and the version it reports', () => {
assert.equal(greeting({ process: { versions: { node: '24.21.0' } } }), 'Hello from Node.js 24.21.0');
});
test('uses the version it finds, not a fixed one', () => {
assert.equal(greeting({ process: { versions: { node: '26.10.0' } } }), 'Hello from Node.js 26.10.0');
});
test('recognises a browser by its document', () => {
assert.equal(greeting({ document: {} }), 'Hello from a browser');
});
test('an empty global object is an unknown host', () => {
assert.equal(greeting({}), 'Hello from an unknown host');
});
test('a process object without versions is not Node.js, and does not throw', () => {
assert.equal(greeting({ process: {} }), 'Hello from an unknown host');
});
test('Node.js wins over a fake document that a test setup added', () => {
assert.equal(greeting({ process: { versions: { node: '24.21.0' } }, document: {} }), 'Hello from Node.js 24.21.0');
}); A hint
Optional chaining reads a property path without throwing: g.process?.versions?.node is undefined as soon as process or versions is missing, so one typeof … === 'string' check covers every case of Node.js. Check Node.js first and the browser second, because a test setup can give a Node.js process a fake document.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · How does Node.js decide whether a file is an ES module or CommonJS?
By the file name first, then by the nearest package.json, then by the code. A .mjs file is always an ES module and
a .cjs file always CommonJS. For a .js file Node.js looks for the nearest package.json above it:
"type": "module" makes the file an ES module and "type": "commonjs" makes it CommonJS. With no type field, or
no package.json at all, Node.js 22.7 and later run the file as CommonJS unless it contains syntax that only an ES
module allows, such as an import or export statement, import.meta or a top-level await; then they run it as an
ES module, which costs a second parse. A good answer adds the practical rule: state "type" in every package.json,
and use .mjs or .cjs for single files that must not depend on where they are saved.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A report script runs fine when you type node report.mjs on a server, but the nightly cron job that runs the same command fails with "TypeError: rows.toSorted is not a function". What is the likely cause, how do you prove it, and how do you stop it from happening again?
-
Fresher to mid
Code that works when pasted into the browser console fails when saved as tool.mjs and run with Node.js 24, first with "ReferenceError: document is not defined" and, after you delete that line, with "ReferenceError: require is not defined in ES module scope". Explain both errors and how you would set the file up.
Key takeaways
- The Run buttons run examples in QuickJS inside your browser, with only
console, timers andqueueMicrotask; their timers keep their order but do not wait. - The browser console runs code in the open page with the browser’s own engine and host objects; paste only code you understand.
- Install Node.js with the official installer or with mise, pin the version per project, and check it with
node --version. .mjsmeans ES module and.cjsCommonJS; a.jsfile follows"type"in the nearestpackage.json, else syntax detection.- The REPL (
node),node -e,node -pandnode --watchcover quick experiments; files are for code you keep.
References
- Node.js 24 documentation: Command-line API (OpenJS Foundation (Node.js))
- Node.js 24 documentation: --watch (OpenJS Foundation (Node.js))
- Node.js 24 documentation: REPL (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Modules, packages (determining the module system) (OpenJS Foundation (Node.js))
- Node.js 24 documentation: process.argv (OpenJS Foundation (Node.js))
- Run Node.js scripts from the command line (OpenJS Foundation (Node.js))
- Download Node.js (OpenJS Foundation (Node.js))
- Node.js previous releases (release schedule) (OpenJS Foundation (Node.js))
- mise use (mise)
- mise exec (mise)
- Open Chrome DevTools (Chrome for Developers)
- What's new in DevTools (Chrome 120): self-XSS warning (Chrome for Developers)
- Web Console (Firefox DevTools user docs) (Mozilla)
- Enabling developer features (Safari) (Apple Developer)
- Inspecting Safari on macOS (Apple Developer)
- quickjs-emscripten (quickjs-emscripten project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress