Warm-up · Activity 1 of 7
// 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.
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()
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");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");Practice · Activity 4 of 7
main.js prints data with console.log and a message with console.error. Match each command to what happens.
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");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);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.jsOutput
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
Hint 1
Ask of each line: would I want it in orders.csv? If not, it belongs on stderr.
Hint 2
console.error and console.warn write to stderr; console.log writes to stdout.
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 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
Hint 1
Number("ten") is NaN, and Number.isNaN tells you so.
Hint 2
Check every argument before you print anything, so a bad one leaves stdout empty.
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.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsCommon 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: 5Why, 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.