Node.js REPL
The Node.js REPL (Read-Eval-Print-Loop) is the interactive shell you get when you run node with no arguments. It lets you type JavaScript straight into a terminal and see results immediately, without creating a file first. Every Node.js install ships with it, and it runs on the exact same V8 engine and core modules as any script you write, which makes it the fastest way to test an idea, inspect an API, or poke at a running process. This lesson covers how the REPL works under the hood, its special commands, how it differs from running a file, common mistakes people make with it, and how to build your own custom REPL with the node:repl module.
Overview / How it works
REPL stands for Read, Eval, Print, Loop — the four steps it repeats for every line you type:
- Read — it reads a line (or several lines) of input from the terminal.
- Eval — it evaluates that input as JavaScript.
- Print — it formats the resulting value with
util.inspectand prints it. - Loop — it redraws the prompt and waits for the next line.
This is fundamentally different from node app.js, which reads an entire file once, runs it top to bottom as a single Node.js module, and exits — it never prints the value of every expression, only what you explicitly log.
When you type node with no arguments, Node.js internally calls repl.start() from the built-in node:repl module with a default configuration. That is the same module you can use later in this lesson to build a custom REPL of your own.
Each REPL session keeps a persistent context across evaluations. A variable declared on one line is still visible on the next line, which is why the REPL feels stateful compared to a script. By default (useGlobal: false), that context is its own object rather than Node’s real global object, so things you define in the REPL do not leak into modules you require — but built-ins like process, console, and require are still available because they are exposed on that context.
The REPL also special-cases top-level let/const: normally you cannot redeclare a const in the same scope, but the REPL evaluates every top-level statement as if it were its own tiny program, so re-running a line like const x = 1 works fine interactively even though the identical two lines in a real file throw a SyntaxError.
Two special variables exist only in the REPL: _ holds the value of the last evaluated expression, and _error holds the last thrown error. Neither exists in a normal script.
Multi-line input is handled by trying to parse each line as it arrives. If the parse is incomplete — an open brace, an unfinished function — the REPL switches to a ... continuation prompt and keeps buffering until the statement is syntactically complete. For pasting larger blocks without fighting that incremental parser, the REPL has a dedicated .editor mode (shown below).
In modern Node.js (16 and later, which covers the current 20/22 LTS lines), the REPL also supports top-level await directly — no flag required — even though a plain CommonJS script cannot use top-level await at all. Input history persists between sessions in ~/.node_repl_history (override the location, or disable history entirely, with the NODE_REPL_HISTORY environment variable), and Tab triggers property/variable name completion.
Syntax
node # start the interactive REPL
node -i # force REPL mode even if stdin is piped/redirected
node -e "1+1" # NOT the REPL — runs one expression and exits
Once inside the REPL, these built-in dot commands are always available:
| Command | What it does |
|---|---|
.help |
Lists all available REPL commands. |
.break / .clear |
Aborts the multi-line statement currently being typed and returns to a fresh prompt. |
.editor |
Enters a mode for pasting multi-line code; press Ctrl+D to run it or Ctrl+C to cancel. |
.save <filename> |
Saves everything typed in the current session to a file. |
.load <filename> |
Reads a file and evaluates its contents into the current session, as if you had typed it. |
.exit |
Exits the REPL (same as Ctrl+D on an empty line). |
Examples
Example 1: Basic expressions and the underscore variable
$ node
Welcome to Node.js v22.11.0.
Type ".help" for more information.
> 1 + 1
2
> const greeting = "Hello, Node!";
undefined
> greeting
'Hello, Node!'
> _
'Hello, Node!'
> .exit
Every expression’s value is printed automatically — you never need console.log for a plain expression. Declarations like const greeting = ... print undefined because a declaration statement has no value, but the assignment still happens, and _ always refers to whatever the last printed value was.
Example 2: Multi-line input with .editor mode
$ node
> .editor
// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)
function square(n) {
return n * n;
}
square(6)
36
> .exit
Typing a multi-line function directly at the normal prompt works too (the ... continuation prompt handles it), but .editor is more forgiving for pasted code because it does not try to evaluate each line as you type — it waits until you signal you are finished with Ctrl+D.
Example 3: Loading a file into the REPL
Save this as greet.js:
function greet(name) {
return `Hello, ${name}!`;
}
module.exports = { greet };
Then load it into a REPL session with .load:
$ node
> .load greet.js
function greet(name) {
return `Hello, ${name}!`;
}
module.exports = { greet };
undefined
> greet("World")
'Hello, World!'
> .exit
.load literally types the file’s contents into the session for you, line by line, so every declaration in it becomes part of your REPL context. This is handy for pulling a helper function into an interactive session without retyping it.
Example 4: Building a custom REPL with node:repl
The node:repl module is not just for the default node command — you can start your own REPL from a script and pre-load it with your application’s objects, which is a common way to build a debug console for a running service.
const repl = require("node:repl");
const users = [
{ id: 1, name: "Ada" },
{ id: 2, name: "Grace" },
];
function findUser(id) {
return users.find((user) => user.id === id);
}
const replServer = repl.start({ prompt: "app> " });
replServer.context.users = users;
replServer.context.findUser = findUser;
replServer.on("exit", () => {
console.log("Debug REPL closed.");
process.exit(0);
});
Output:
$ node debug-console.js
app> users
[ { id: 1, name: 'Ada' }, { id: 2, name: 'Grace' } ]
app> findUser(2)
{ id: 2, name: 'Grace' }
app> .exit
Debug REPL closed.
repl.start() returns a REPL server object. Anything you assign onto replServer.context becomes a variable available inside that session, which is exactly how you would expose a live database connection, an in-memory cache, or application state to whoever is debugging the process interactively.
How it works step by step / Under the hood
- The REPL’s
readlineinterface captures keystrokes from the terminal, line by line. - After each line, the REPL attempts to parse the buffered input as a complete JavaScript program. If the parse fails because the statement is unfinished (an open
{, an unterminated string), it switches to the...continuation prompt and keeps reading instead of throwing. - Once the input parses as a complete statement or expression, the REPL evaluates it in its context (via the same machinery as
node:vm), capturing the completion value of the last expression — this is what lets a bare2 + 2produce a printed result the way a normal script never would. - The result is assigned to
_(or the error to_errorif the evaluation threw), then formatted withutil.inspect— which is why objects print with their properties expanded, strings print with quotes, andundefinedis shown explicitly. - The formatted value is written to stdout, the prompt is redrawn, and the loop waits for the next line.
Because this all runs on Node’s single main thread like any other code, a long synchronous computation typed into the REPL blocks the prompt exactly the way it would block a server’s event loop — the REPL gets no special scheduling privileges.
Common Mistakes
Mistake 1: Assuming REPL rules match script rules
The REPL relaxes redeclaration because it treats each top-level statement independently. Copying REPL-tested code straight into a file can break:
const count = 1;
const count = 2;
// SyntaxError: Identifier 'count' has already been declared
This works fine typed as two separate lines in the REPL, but as a single file it is a parse-time SyntaxError. If you actually need to reassign a value, use let:
let count = 1;
count = 2;
Mistake 2: Forgetting require() resolves against the current directory
The REPL has no source file of its own, so a relative require() resolves against process.cwd() — wherever you happened to launch node from — not against your project root:
$ cd ~/Desktop
$ node
> require("./utils").helper()
Uncaught Error: Cannot find module './utils'
> .exit
$ cd ~/projects/my-app
$ node
> require("./utils").helper()
'ready'
> .exit
If a module you expect to find is not resolving, check where you started the REPL before assuming the module is broken.
Mistake 3: Not knowing top-level await just works
Older advice mentions an --experimental-repl-await flag; on current LTS releases that flag is unnecessary because top-level await is supported by default:
$ node
> const res = await fetch("https://api.github.com");
undefined
> res.status
200
> .exit
A related gotcha is exiting: Ctrl+C once cancels the line or multi-line statement you are currently typing; pressing it again on an empty line (or Ctrl+D anywhere) actually exits the process. Mistaking the first Ctrl+C for a hang, and mashing it repeatedly mid-statement, is a common source of confusion for beginners.
Best Practices
- Use the REPL to experiment and inspect — not to write or maintain real application logic; anything worth keeping belongs in a file.
- Prefer
.editormode when pasting more than a couple of lines, so the incremental parser does not misinterpret partial input. - Remember
_only holds the last printed value — assign results to a named variable if you need them a few steps later. - Start the REPL from your project’s root directory if you plan to
require()local files. - Use
.saveto capture a useful exploratory session before you forget what you typed. - When building a custom REPL for debugging a live service, only expose what you actually need on
replServer.context, and never attach a REPL like this to a process reachable from outside your machine. - Lean on Tab completion to discover methods on an object you are inspecting for the first time.
Practice Exercises
- Open a plain
nodeREPL, build an array of five numbers, and use.reduce()to sum it. Confirm the sum by typing_on the next line. - Use
.editormode to define anasyncfunction that awaitsfetchagainst any public API and returns the parsed JSON, then call it and inspect the result. - Write a script that calls
repl.start()with a custom prompt, attaches a small in-memory array of objects toreplServer.context, and adds afindByIdhelper function to the context as well. Run it and query the array interactively.
Summary
- The REPL reads, evaluates, prints, and loops — it is not the same execution model as running a script file.
- Its context persists across lines within one session, which is why variables stick around and why
constredeclaration is tolerated there but not in a file. _holds the last printed value;_errorholds the last thrown error.- Dot commands (
.help,.editor,.load,.save,.break/.clear,.exit) control the session itself, not JavaScript evaluation. - Top-level
awaitworks directly in the REPL on modern Node.js, with no flag needed. - Relative
require()paths resolve against the directory you launchednodefrom, since the REPL has no source file of its own. node:replis a real module you can use to build a custom interactive debug console into your own applications.
