Running JavaScript with Node

Published Updated

By the end of this page you will be able to check your Node install, run a JavaScript file from a terminal, understand what the "type" field changes, read the arguments somebody typed after your filename, send output to the right stream, and leave an exit code the terminal can act on.

Every output block below was produced by running the code on Node 24.16.0, and each block says so. Where a claim depends on your operating system or on an installer screen, the page says that too rather than inventing a result.

Confirming Your Install and Reading the Version

Start by asking whether Node is already there. Two commands answer it, and neither changes anything:

node --version
which node
// terminal output on the machine this page was written on:
// v24.16.0
// /Users/example/.nvm/versions/node/v24.16.0/bin/node

The second line shows which copy of Node you are actually running, which matters once more than one is installed. The path shown here comes from a version manager, so yours will differ.

On Windows the equivalent is where.exe node or (Get-Command node).Source in PowerShell. Write where.exe rather than bare where, because PowerShell uses where as an alias for Where-Object; bare where node is correct in cmd.exe.

If the shell instead reports an unknown command, saying command not found on macOS and Linux or that the term is not recognized in PowerShell, Node is not installed or is not on the list of places your shell searches.

In a POSIX shell the exit code for that is 127, which is that specification's convention rather than a universal value. PowerShell raises a CommandNotFoundException instead, and its $LASTEXITCODE only tracks native programs and PowerShell scripts.

Version numbers read as major, minor, patch. Through Node 26, only even-numbered majors became Long Term Support releases and received fixes for years; from Node 27 every major is planned to reach Long Term Support.

Either way the practical advice is the same: take a currently supported Long Term Support release. Installers live at nodejs.org, and their screens are not described here because this page can only report what it ran.

Running Your First File

Put one line in a file called hello.js:

console.log("Hello from Node");

Then, in a terminal whose current folder is the folder holding that file, run it:

node hello.js
// node 24.16.0 output:
// Hello from Node

Node read the file, ran it top to bottom, printed the line, and ended. There is nothing else running afterwards and nothing to shut down.

If the answer is a MODULE_NOT_FOUND error naming your file, Node looked in the folder your terminal is in rather than the one your editor has open. List the folder contents and check the name, including the .js extension.

What "type": "module" Changes

JavaScript has two module systems. Which one a .js file uses is decided by the nearest package.json when that file supplies a "type", and by syntax detection when nothing does. A .mjs or .cjs extension overrides both.

Setting the field is one line:

{
  "type": "module"
}

With that in place, a .js file uses import and export, and require stops existing. That is not a warning but a hard error, and the message is unusually helpful:

const fs = require("node:fs");
// node 24.16.0 output:
// ReferenceError: require is not defined in ES module scope, you can use import instead
// This file is being treated as an ES module because it has a '.js' file extension
// and package.json contains "type": "module". To treat it as a CommonJS script,
// rename it to use the '.cjs' file extension.

One current behaviour is worth stating precisely, because a great deal of older advice contradicts it. On Node 24, a .js file containing import with no package.json deciding the matter still runs, because Node inspects the syntax and treats it as a module:

import { shout } from "./util.js";
console.log(shout("hello"));
// node 24.16.0 output, with no package.json present:
// HELLO

That is syntax detection, and it has a boundary worth knowing. It arrived behind a flag in Node 20.10 and 21.1, and is on by default from Node 20.19 and 22.7, so older tutorials calling that exact file a syntax error were right for their version.

It applies only to input that is otherwise ambiguous, and only certain syntax triggers it: a static import or export, import.meta, top-level await, and some redeclarations. A dynamic import() does not, and a file using only import() stays CommonJS, which was confirmed here by checking that require was still a function in it.

Node also documents the feature as release-candidate status carrying a performance cost, since detection means parsing a file twice when the first attempt fails. Declaring "type": "module" is therefore still the better habit: it states the intent, avoids the detection pass, and behaves the same on every version.

Two extensions override the field whenever you need the other system in one file. A .mjs file is always a module, a .cjs file is always CommonJS, and a module can import a .cjs file.

Top-level await works in module files, including a plain .js file detected as one. The modules guide covers the import syntax itself.

Reading What the Person Typed

Anything typed after the filename arrives in process.argv. It is a plain array of strings, and its first two entries are always the Node binary and your script:

console.log(process.argv);
// node 24.16.0 output, run as: node greet.js Ada --loud
// [
//   '/Users/example/.nvm/versions/node/v24.16.0/bin/node',
//   '/private/tmp/example/greet.js',
//   'Ada',
//   '--loud'
// ]

Almost nobody wants those first two, so the usual first line slices them off:

const args = process.argv.slice(2);
console.log("arguments:", args);
console.log("first:", args[0]);
// node 24.16.0 output, run as: node greet2.js Ada --loud
// arguments: [ 'Ada', '--loud' ]
// first: Ada

Run the same file with nothing after it and the array is empty, so args[0] is undefined rather than an error. That silence is why the next section matters: a script that quietly proceeds with undefined is worse than one that stops and says what it needed.

Everything in that array is a string, including numbers, so "3" needs converting before you do arithmetic with it. For a flag, args.includes("--dry-run") is enough; Node's util.parseArgs handles the cases where it is not.

The Two Output Streams

A program has two ways out. Standard output carries the result, and standard error carries messages about the run, which is why console.log and console.error are not just two colours of the same thing:

console.log("this goes to standard output");
console.error("this goes to standard error");
process.stdout.write("no newline of its own");
process.stdout.write("\n");

Run it plainly and all three lines appear together, because the terminal shows both streams. Send the output to a file and they separate:

node streams.js > out.txt
// node 24.16.0, still shown in the terminal:
// this goes to standard error
//
// and out.txt now contains:
// this goes to standard output
// no newline of its own

That is the whole reason for the distinction. Progress notes and warnings belong on standard error so they stay visible when somebody captures your real output, and the same applies when the output is piped into another command.

console.log adds a newline and formats objects for reading; process.stdout.write does neither, which suits building a line in pieces.

Exit Codes, the Answer Nobody Prints

Every run leaves a number behind. Zero means it worked and anything else means it did not, and you read it with echo $? immediately after the command on macOS and Linux.

On Windows PowerShell the equivalent is $LASTEXITCODE.

const args = process.argv.slice(2);
if (args.length === 0) {
  console.error("Usage: node check.js <name>");
  process.exit(1);
}
console.log("Checking", args[0]);
// node 24.16.0 output:
// $ node check.js
// Usage: node check.js <name>
// exit code: 1
//
// $ node check.js report.txt
// Checking report.txt
// exit code: 0

You get the same signal without asking for it. An uncaught error prints a stack trace and exits with 1, and so does a missing file, both confirmed on 24.16.0. A script that ran to the end with no error exits with 0 on its own, so there is nothing to add on the successful path.

Node's own guidance prefers setting the code over forcing the exit, because process.exit can cut off output that has not been written yet. Assign process.exitCode and let the script finish on its own:

process.exitCode = 1;
console.log("work finished, leaving a failure code");
// node 24.16.0 output:
// work finished, leaving a failure code
// exit code: 1

Keep the forced process.exit(1) for the case in the example above, where you want to stop before doing any work at all. When something must stop mid-run, throwing is safer than forcing the process down.

This is the number every other tool reads. A conditional chain such as a && b stops when one link exits non-zero, though commands separated by semicolons carry on regardless, and PowerShell has supported && only since version 7.

A continuous integration job reports a failure on exactly the same evidence. Printing the word failed while exiting 0 tells a person one thing and every machine the opposite.

Three Flags Worth Knowing Early

node -e runs a snippet with no file at all, which is the quickest way to check what something returns. node -p does the same and prints the result:

node -p '2 + 2'
// node 24.16.0 output:
// 4

node --check parses a file without running it, which answers whether a problem is a typo or a logic error. It exits 0 on a file that parses and 1 on one that does not, both confirmed here, and it reports the line and column of the first syntax error.

Pitfalls and Debugging

Cannot find module, naming your own file. The likeliest cause is the terminal being in a different folder from the file. Check with the folder-listing command, and check the extension too, since hello and hello.js are different names to Node.

require is not defined. The file is being treated as a module, whether by a "type": "module" field nearby, an .mjs extension, or syntax detection reading an import already in the file. Convert the line to import, or rename the file to .cjs if it genuinely needs the older system.

Cannot use import statement outside a module. This is the mirror image, and on Node 24 it means something actively decided against modules, most often a .cjs extension. Syntax detection only applies when nothing else has decided.

An argument arrives as a string. Everything in process.argv is text, so a typed 3 is "3" and adding to it concatenates. Convert with Number() and check the result is not NaN, as the values guide covers.

Invalid package config. A trailing comma or a stray quote in package.json stops Node before your file runs at all, with an error naming the file rather than any line of yours. JSON allows no trailing commas and no comments.

The script printed an error and the pipeline carried on. Printing is not failing. Call process.exit(1), or throw, so the exit code matches what the message says.

Frequently Asked Questions

Why does node file.js say cannot find module?

Node looked for that filename relative to the folder your terminal is in, and did not find it. Run the folder listing command first and check the name is there, spelled the same way, including its extension. The same message also appears when an import inside the file points at a path that does not resolve.

Do you have to add type module?

On Node 24 a plain .js file containing import or export usually runs as a module anyway, because Node inspects the syntax when no type field decides it. Declaring it is still worth doing, since it states the intent, removes the guesswork, and behaves the same on older versions that had no detection.

How do you read flags like dry-run properly?

For one or two flags, checking whether the argument array includes them is enough and adds nothing to your project. Once you want values, short forms, and a usage message, Node has a built-in parser in its util module. Reach for an argument-parsing package only when neither of those covers what you need.

Should a script call process.exit at the end?

Not on the successful path, because a script that finishes normally already exits with zero. Calling it early can also cut off output that has not been flushed yet. Node's own guidance is to set process.exitCode and let the script end naturally, keeping a forced exit for when you truly must stop immediately.

Sources

  1. [1]
    Command-Line API
    (nodejs.org)
  2. [2]
    Process
    (nodejs.org)
  3. [3]
    Modules: Packages
    (nodejs.org)