You worked out how to call an API in the REPL, it took twenty minutes, and the useful part is fourteen lines somewhere in a history of forty. Tomorrow you will do it again from memory, get a field name wrong the same way you did the first time, and spend another twenty minutes.
What you get
You will end up with a script built from the session, with the failed attempts, and the lines you typed only to look at something, removed. This is for you if you explore APIs interactively and lose the result.
Short answer
Keep the declarations and the calls. Drop three things: any line that threw when you typed it, any bare expression typed only to see a value, and the REPL’s own dot commands. Where a name was declared twice, keep the later version in the position the first one held. Then read the result, because a working session is not the same as a script you would commit.
You will need
Node 22 or later, and a REPL session whose history you kept. Node writes one by default to the path
NODE_REPL_HISTORY names,1 and
.save writes the current session to a file if you remember to run it before you exit. Python’s
readline history works the same way, and so does
the conversion.2
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| .save in the Node REPL | You remember to run it, and the session was mostly clean | Everything you typed, failures included, so the file rarely runs as written | The session had false starts, which is most sessions |
| Converting the history file | Any session, including the ones you did not plan to keep | A conversion that guesses, and output you have to read before trusting | You want the script to be what you would have written |
| Copying from the terminal | Three or four lines that you can see on one screen | Prompts and output mixed into the code, removed by hand | The session is longer than a screen |
| Writing the script from scratch | The exploration taught you something and the code is worth rewriting | The twenty minutes again, from a memory that is already fading | You want the thing that worked, verbatim |
The conversion is a first draft and it is worth being clear about that. What it gives you is every line that ran, with the noise gone, which is a much better starting point than a blank file and a memory. What it does not give you is a script you should commit unread.
.save and a history conversion differ in when you have to have decided.3 .save needs the thought
before you exit; a history file is there whether or not you planned anything. Given that the whole
situation starts with an unplanned session, the second is the one that works in practice.
Drop the three kinds of noise
Three rules, each aimed at a different thing a REPL collects.
if (line.startsWith('.')) {
dropped.push({ line, why: 'REPL command' })
continue
}
if (ran(line) === false) {
dropped.push({ line, why: 'threw when it was typed' })
continue
}
if (isBareExpression(line)) {
dropped.push({ line, why: 'typed to look at a value' })
continue
}
Report what was dropped and why. A conversion that silently removes a line you needed is worse than one that keeps too much, and the list is how you check its work in five seconds.
Bare expressions are the interesting case. In a REPL, typing page.data.length prints a number and
is the point of typing it. In a script it computes a value and discards it, which is dead code that
looks like an intention.
Keep the version that worked
A name declared twice keeps its later definition.
function dedupe(lines) {
const byName = new Map()
const order = []
for (const line of lines) {
const name = /^(?:const|let|var)\s+([A-Za-z_$][\w$]*)/.exec(line)?.[1]
if (!name) { order.push({ line }); continue }
if (byName.has(name)) byName.get(name).line = line
else {
const entry = { line }
byName.set(name, entry)
order.push(entry)
}
}
return order.map((e) => e.line)
}
Keeping the first position and the last definition is what makes the result run. A redeclaration
moved to the end would leave every reference between the two reading an older value. In a module a
second const with the same name is a syntax error rather than a shadow.
Check it worked
Convert a session that had three false starts in it.
node demo.mjs
dropped
threw when it was typed res.staus
typed to look at a value res.status
threw when it was typed page.data.lenght
typed to look at a value page.data.length
typed to look at a value ids
threw when it was typed const serials = page.data.map(m => m.serail)
REPL command .exit
script
const base = 'http://127.0.0.1:0'
const key = 'sk_live_1'
await fetch(base + '/meters')
const res = await fetch(base + '/meters', { headers: { authorization: 'Bearer ' + key } })
const page = await res.json()
const ids = page.data.map(m => m.id)
const serials2 = page.data.map(m => m.serial)
console.log(ids, serials2)
Eight lines from a history of fifteen, and the three typos are gone. Read the script before you keep
it, because it still carries the evidence of how it was made. Line three is an unauthenticated probe
from when you were checking the endpoint existed. Line two holds a live key that belongs in the
environment. The last variable is called serials2 because the first attempt failed.
The dropped list is the other half of the evidence. It gives the reason for each of the seven lines that went. A conversion you cannot audit is a conversion you will stop using the first time it removes something you wanted.
node --test convert.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 110.048599
When it goes wrong
The script fails on a name that does not exist. A declaration was typed as a bare expression, or the REPL had it from an earlier session. Run the script in a clean process before you trust it. A REPL carries everything you ever typed, and a script carries only what you kept.
Multiline input becomes several broken lines. History files store what you typed per line, and a function body spans several. Join continuation lines before converting, or keep multiline work in a file from the start. A function you are still shaping belongs in an editor, and the REPL is for calling it.
A credential ends up in the script. You typed it in the REPL, so it is in the history file too. Move it to an environment variable, and consider whether the history file itself needs deleting.
Nothing survives the conversion. The session was all inspection and no declarations. That is a finding about the session rather than the tool. It usually means the work was reading rather than building, and there was nothing to save.
When not to do this
Do not commit a converted session without reading it. The output is a transcript of how you learned, and a transcript is not a design. Rename the variables, remove the exploratory calls, and it becomes a script somebody else can read. Ten minutes of that is the difference between a note to yourself and something the team can use.
Do not convert a session that touched production. The lines that ran are exactly the lines you would rather not re-run by accident, and a file makes that one keystroke away. Convert it, read it, then decide whether it should exist at all.
Do not treat the history file as private. It is a plain file, it is often backed up, and it holds every key you pasted while exploring.
Related how-tos
Last verified
Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.
Footnotes
-
The default file is
.node_repl_historyin the home directory, andNODE_REPL_HISTORY_SIZEkeeps the last 1000 lines of it. SettingNODE_REPL_HISTORYto an empty string switches the history off, except on Windows, where an environment variable with an empty value is invalid. The documentation’s remedy is to set it to one or more spaces, and it offers this without further comment. ↩︎ Back to text -
Python has saved its history to
~/.python_historysince 3.4, when the site module made the behavior automatic. The readline module adds a note for macOS, where the library underneath may belibeditrather than GNU readline. The two may use different history file formats, and switching between them may leave the existing file unusable. The same way, then, up to the point where it is not. ↩︎ Back to text -
The Node REPL has seven special commands:
.break,.clear,.exit,.help,.save,.loadand.editor, and one of the seven exists to show the list of the other six..saveand.loadare documented as a pair, each with a one-line example..editorgets the longest entry, in which a function greets a Node.js user and Ctrl+D finishes the job. ↩︎ Back to text