Skip to content
aviral gupta

// B1.4 · ~35 min · Beginner

stdout, stderr and exit codes

After this lesson your programs keep their results apart from their messages, so a file or the next program gets only data, and they tell the shell with an exit code whether they succeeded.

Lesson 4 of 5 in B1 Running Node and project basics

You will be able to

  • Send data to stdout and messages to stderr, and predict what >, 2> and | do with each
  • Write exact output with process.stdout.write, without an automatic newline
  • Report success or failure with an exit code, read it in the shell, and prefer process.exitCode to process.exit()
  1. Warm-up · Activity 1 of 7

    Warm-up: which of these console methods write to stderr, not stdout? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on. You run node main.js > fruit.txt to save the fruit list. What ends up in fruit.txt?

    console.log("Reading 3 files...");
    console.log("apple,3");
    console.error("warning: pears.txt is empty");
    console.log("plum,7");
  3. Practice · Activity 3 of 7

    Fill in the method so that the warning stays out of fruit.txt when the program runs as node main.js > fruit.txt, and still shows on the screen.

    console.____("warning: pears.txt is empty");
    console.("warning: pears.txt is empty");
  4. Practice · Activity 4 of 7

    main.js prints data with console.log and a message with console.error. Match each command to what happens.

  5. Practice · Activity 5 of 7

    What does this program print?

    process.stdout.write("Loading");
    process.stdout.write(".");
    process.stdout.write(".");
    console.log(" done");
    console.log("2 files");
  6. Brain teaser · Activity 6 of 7

    Brain teaser. On Linux you run node big.js | wc -c, which counts the characters the program wrote. What does it print? (Written to a file with > instead, all 1000000 arrive.)

    process.stdout.write("x".repeat(1000000));
    process.exit(0);
  7. Apply · Activity 7 of 7

    Mini-task. Write report.js: for each text in ["12", "7", "x", "30"] print the number doubled with console.log, or, if it is not a number, print not a number: x with console.error. If any text was bad, set process.exitCode = 1. Run node report.js > rows.txt, then check the exit code: echo $? in bash, or $LASTEXITCODE in PowerShell. Open rows.txt.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A converter that keeps its data clean

This program turns Celsius readings into a CSV file with a Fahrenheit column. The CSV goes to stdout, partly with process.stdout.write so that each row is built from two pieces. Everything meant for a person goes to stderr: a progress line, a warning for the reading that is not a number, and a summary. The output below is stdout alone, exactly what node main.js > temps.csv saves. A skipped reading is only a warning here, so the exit code stays 0.

main.js

// Converts Celsius readings to Fahrenheit, as CSV.
// Data goes to stdout, messages go to stderr.
const readings = ["20", "-5", "warm", "37.5"];

console.error("Converting " + readings.length + " readings...");
process.stdout.write("celsius,fahrenheit\n");
let skipped = 0;
for (const text of readings) {
  const celsius = Number(text);
  if (Number.isNaN(celsius)) {
    console.warn("skipped " + text + ": not a number");
    skipped++;
    continue;
  }
  process.stdout.write(celsius + ",");
  process.stdout.write((celsius * 9) / 5 + 32 + "\n");
}
console.error("Done: " + (readings.length - skipped) + " converted, " + skipped + " skipped.");

Run it with

node main.js

Output

celsius,fahrenheit
20,68
-5,23
37.5,99.5
  • In the terminal, node main.js also shows three stderr lines around the CSV: Converting 4 readings..., skipped warm: not a number, and Done: 3 converted, 1 skipped.
  • node main.js > temps.csv writes only the four CSV lines into the file; the three messages stay on the screen.
  • Each row comes from two write calls: the first leaves the line open after the comma, the second ends it with "\n".
  • celsius + "," is a string, so write accepts it; write(celsius) alone would throw a TypeError.

Exercises

Exercise 1 of 2

Only CSV on stdout

Someone runs node main.js > orders.csv, so stdout must hold only CSV: first the header line id,amount, then one line per order with an amount. Right now the progress and warning lines land in the file too, and the header is missing. Print the header with console.log, and send the three messages to stderr with console.error or console.warn instead of deleting them.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Ask of each line: would I want it in orders.csv? If not, it belongs on stderr.

  2. Hint 2

    console.error and console.warn write to stderr; console.log writes to stdout.

  3. Hint 3

    Add console.log("id,amount"); before the loop.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// Prints one CSV line per order: id,amount.
// Someone runs node main.js > orders.csv, so stdout must hold only CSV.
const orders = [["A-1", 12.5], ["A-2", null], ["A-3", 7.25]];

console.error("Exporting orders...");
console.log("id,amount");
for (const [id, amount] of orders) {
  if (amount === null) {
    console.warn("warning: " + id + " has no amount, skipped");
    continue;
  }
  console.log(id + "," + amount.toFixed(2));
}
console.error("Export finished.");
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Prints one CSV line per order: id,amount.
// Someone runs node main.js > orders.csv, so stdout must hold only CSV.
const orders = [["A-1", 12.5], ["A-2", null], ["A-3", 7.25]];

console.log("Exporting orders...");
for (const [id, amount] of orders) {
  if (amount === null) {
    console.log("warning: " + id + " has no amount, skipped");
    continue;
  }
  console.log(id + "," + amount.toFixed(2));
}
console.log("Export finished.");

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';

test('stdout starts with the header id,amount', async () => {
  const first = (await runMain()).split('\n')[0];
  assert.equal(first, 'id,amount', `the first line on stdout is ${JSON.stringify(first)}`);
});

test('no progress or warning line on stdout', async () => {
  const out = await runMain();
  assert.doesNotMatch(out, /Export|warning/, `stdout still holds a message: ${JSON.stringify(out)}`);
});

test('stdout is exactly the header and the two orders', async () => {
  const got = (await runMain()).trimEnd().split('\n');
  assert.deepEqual(got, ['id,amount', 'A-1,12.50', 'A-3,7.25'], `stdout is ${JSON.stringify(got)}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

An exit code for the shell

node main.js 4 2.5 10 should print the sum, 16.5, and no arguments should print 0. If any argument is not a number, such as ten, the program must print a message like not a number: ten to stderr, print nothing to stdout, and end with exit code 1, so that node main.js 4 ten && echo ok prints no ok. Use process.exitCode, not process.exit(). Check with node --test.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    Number("ten") is NaN, and Number.isNaN tells you so.

  2. Hint 2

    Check every argument before you print anything, so a bad one leaves stdout empty.

  3. Hint 3

    For a bad argument: console.error("not a number: " + arg); then process.exitCode = 1; and skip the console.log of the total.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// node main.js 4 2.5 10  ->  prints 16.5
// If an argument is not a number: a message on stderr, nothing on stdout, exit code 1.
const args = process.argv.slice(2);
const bad = args.filter((arg) => Number.isNaN(Number(arg)));
if (bad.length > 0) {
  console.error("not a number: " + bad.join(", "));
  process.exitCode = 1;
} else {
  let total = 0;
  for (const arg of args) {
    total += Number(arg);
  }
  console.log(total);
}
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// node main.js 4 2.5 10  ->  prints 16.5
// If an argument is not a number: a message on stderr, nothing on stdout, exit code 1.
const args = process.argv.slice(2);
let total = 0;
for (const arg of args) {
  total += Number(arg);
}
console.log(total);

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';

test('4 2.5 10 prints 16.5', async () => {
  const got = (await runMain({args: ['4', '2.5', '10']})).trimEnd();
  assert.equal(got, '16.5', `the program printed ${JSON.stringify(got)}`);
});

test('no arguments print 0', async () => {
  const got = (await runMain()).trimEnd();
  assert.equal(got, '0', `the program printed ${JSON.stringify(got)}`);
});

test('4 ten ends with an exit code other than 0', async () => {
  await assert.rejects(runMain({args: ['4', 'ten']}), Error, 'node main.js 4 ten ended with exit code 0');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Writing a number with process.stdout.write

const count = 42;
process.stdout.write(count);

What Node.js prints

TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received type number (42)

Why, and the fix

console.log turns any value into text, but process.stdout.write takes only a string (or bytes). Convert the number yourself and add the newline: process.stdout.write(count + "\n") or process.stdout.write(String(count)).

Passing several values to process.stdout.write

const total = 5;
process.stdout.write("Total:", total);

What Node.js prints

TypeError [ERR_UNKNOWN_ENCODING]: Unknown encoding: 5

Why, and the fix

Unlike console.log, write does not join its arguments: the second one is the encoding of the text, such as "utf8", so 5 is an unknown encoding. Build one string first: process.stdout.write("Total: " + total + "\n").

Passing a message to process.exit()

console.log("checking...");
process.exit("config file missing");

What Node.js prints

TypeError [ERR_INVALID_ARG_TYPE]: The "code" argument must be of type number. Received type string ('config file missing')

Why, and the fix

process.exit() takes an exit code, a whole number, not a message (since Node.js 20 only a number or a string of digits). Print the message to stderr, then set the code: console.error("config file missing"); process.exitCode = 1;

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Two streams: stdout for data, stderr for messages

Every program gets two output streams. console.log, console.info and console.debug write to stdout; console.error, console.warn and console.trace write to stderr. A terminal shows both, mixed, but the shell can split them: node main.js > out.txt saves only stdout, and the stderr lines still appear on the screen; 2> err.txt saves only stderr; node main.js | sort passes only stdout to the next program. So keep the result on stdout and everything meant for a person on stderr: progress, warnings, usage and errors. The report of an uncaught error goes to stderr too, so out.txt holds only what was printed before the crash.

process.stdout.write prints exactly what you give it

console.log formats its arguments, adds a newline and writes the result to process.stdout, the stream behind stdout. process.stdout.write(text) writes the text and nothing more: no newline, no spaces between parts, no formatting. Use it to build one line from several pieces, and add "\n" yourself where a line ends. It takes a string, not a number: write(42) throws a TypeError. A second argument is read as the encoding of the text, so write("Total:", 5) fails with Unknown encoding: 5. process.stderr.write does the same for stderr. The browser has no process object, so code that uses either runs only on your computer.

The exit code is the verdict

When a program ends, the shell receives its exit code: 0 means success, anything else failure. Scripts and tools read the code, not your text: node check.js && node deploy.js runs the second command only after exit code 0. In bash, echo $? prints the last exit code; in PowerShell, $LASTEXITCODE holds it. Set the verdict with process.exitCode = 1 and let the program finish. process.exit(code) ends the process at once, with code, or with process.exitCode when you pass none. It does not wait for pending work, and on Linux and macOS that includes output still on its way into a pipe: node big.js | wc -c can count less than was written.

Sources

Last reviewed September 30, 2026