Interacting with the file system
Standard in (stdin) refers to an input stream that a program can use to read input from a command shell or Terminal. Similarly, standard out (stdout) refers to the stream that is used to write the output. Standard error (stderr) is a separate stream to stdout that is typically reserved for outputting errors and diagnostic data.
In this recipe, we’re going to learn how to handle input with stdin, write output to stdout, and log errors to stderr.
Getting ready
For this recipe, let’s first create a single file named greeting.js. The program will ask for user input via stdin, return a greeting via stdout, and log an error to stderr when invalid input is provided. Let’s create a directory to work in, too:
$ mkdir interfacing-with-io $ cd interfacing-with-io $ touch greeting.js
Now that we’ve set up our directory and file, we’re ready to move on to the recipe steps.
How to do it…
In this recipe, we’re going to create a program that can read from stdin and write to stdout and stderr:
- First, we need to tell the program to listen for user input. This can be done by adding the following lines to
greeting.js:console.log('What is your name?'); process.stdin.on('data', (data) => { Â Â // processing on each data event }); - We can run the file using the following command. Observe that the application does not exit because it is continuing to listen for
process.stdindata events:$ node greeting.js
- Exit the program using Ctrl + C.
- We can now tell the program what it should do each time it detects a data event. Add the following lines below the
// processing on each dataeventcomment:Â Â const name = data.toString().trim().toUpperCase(); Â Â process.stdout.write(`Hello ${name}!`); - You can now type input to your program. When you press Enter, it will return a greeting and your name in uppercase:
$ node greeting.js What is your name? Beth Hello BETH!
- We can now add a check for whether the input string is empty and log to
stderrif it is. Change your file to the following:console.log('What is your name?'); process.stdin.on('data', (data) => {   // processing on each data event   const name = data.toString().trim().toUpperCase();   if (name !== '') {     process.stdout.write(`Hello ${name}!`);   } else {     process.stderr.write('Input was empty.\n');   } }); - Run the program again and hit Enter with no input:
$ node greeting.js What is your name? Input was empty.
We’ve now created a program that can read from stdin and write to stdout and stderr.
How it works…
The process.stdin, process.stdout, and process.stderr properties are all properties on the process object. A global process object provides information and control of the Node.js process. For each of the I/O channels (standard in, standard out, standard error), they emit data events for every chunk of data received. In this recipe, we were running the program in interactive mode where each data chunk was determined by the newline character when you hit Enter in your shell.
The process.stdin.on('data', (data) => {...}); instance is what listens for these data events. Each data event returns a Buffer object. The Buffer object (typically named data) returns a binary representation of the input.
The const name = data.toString() instance is what turns the Buffer object into a string. The trim() function removes all whitespace characters – including spaces, tabs, and newline characters – from the beginning and end of a string. The whitespace characters include spaces, tabs, and newline characters.
We write to stdout and stderr using the respective properties on the process object (process.stdout.write, process.stderr.write).
During the recipe, we also used Ctrl + C to exit the program in the shell. Ctrl + C sends SIGINT, or signal interrupt, to the Node.js process. For more information about signal events, refer to the Node.js Process API documentation: https://nodejs.org/api/process.html#process_signal_events.
Important note
Console APIs: Under the hood, console.log and console.err are using process.stdout and process.stderr. Console methods are higher-level APIs and include automatic formatting. It’s typical to use console methods for convenience and lower-level process methods when you require more control over the stream.
There’s more…
As of Node.js 17.0.0, Node.js provides an Experimental Readline Promises API, which is used for reading a file line by line. The Promises API variant of this allows you to use async/await instead of callbacks, providing a more modern and cleaner approach to handling asynchronous operations.
Here is an example of how the Promises API variant can be used to create a similar program to the greeting.js file created in the main recipe:
const readline = require('node:readline/promises');
async function greet () {
  const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      });
  const name = await rl.question('What is your name?\n');
  console.log(`Hello ${name}!`);
  rl.close();
}
greet(); This Node.js script utilizes the node:readline/promises module, which provides the Promise variant of the Readline API. It defines an asynchronous function, greet(), which prompts the user for their name in the console and then greets them with a personalized message – similar to the main recipe program. Using the Readline Promises API allows us to use the async/await syntax for cleaner asynchronous code flow. We’ll cover more about the async/await syntax in later recipes and chapters.
See also
- The Decoupling I/O recipe in Chapter 3