Stdin, Stdout, and Stderr Explained: A Beginner's Guide to Terminal Streams

Stdin, Stdout, and Stderr Explained: A Beginner's Guide to Terminal Streams

Fi Amanillah
Fi Amanillah AUTHOR
Full Stack Developer
7 MIN READ
72 Views
Cover illustration for Stdin, Stdout, and Stderr Explained: A Beginner's Guide to Terminal Streams
// COVER IMAGE: Stdin, Stdout, and Stderr Explained: A Beginner's Guide to Terminal Streams

#1. Introduction

Have you ever run a command in your terminal, tried to save the output to a file with >, and then opened that file only to find half the text missing — while the other half mysteriously still printed to your screen?

If that sounds familiar, you've just collided with one of the oldest, most elegant, and occasionally most confusing ideas in computing: standard streams.

Let's pop the hood on stdin, stdout, and stderr, understand why they exist, and see how to use them properly in your own code (with a bit of TypeScript along the way).

// NOTE
This guide is written for developers of any level — no prior systems programming knowledge required.

#2. Why Do Streams Even Exist?

#2.1 The Unix Philosophy: "Everything Is a File"

To understand standard streams, we need to rewind to the 1970s. The creators of Unix made a bold, slightly unusual design choice: "Everything is a file."

Rather than writing separate logic for talking to a hard drive, a keyboard, a monitor, or a network socket, Unix-like systems (Linux, macOS) just treat all of them the same way — as files. Reading from a keyboard works the same way as reading from data.txt. Writing to a monitor works the same way as writing to log.txt.

#2.2 File Descriptors: Your Program's Tickets

When a program starts, the operating system creates a "file descriptor table" for it. Think of a file descriptor (FD) as a ticket number that points to something the program can read from or write to. Every program gets handed three tickets automatically, the moment it launches:

FD Stream Think of it as... Default Destination
0 stdin The program's ears Your keyboard
1 stdout The program's normal voice Your screen
2 stderr The program's panic voice Your screen

#2.3 The Genius Part: Programs Don't Know Where They're Writing

Here's the clever bit: the program itself has no idea where these streams actually lead. A TypeScript script doesn't know whether stdout is landing on a monitor, being saved to a file, or traveling across the internet to another server. It just writes to FD 1 and lets the operating system handle the plumbing.


#3. The Secret Trick: How Your Shell Redirects Output

#3.1 What Happens When You Run a Command

This is the part most tutorials skip — and it's the key to understanding why > works at all.

When your shell (like bash or zsh) runs a command, it doesn't just summon a program out of nowhere. It performs two steps:

  1. fork() — The shell clones itself into a near-identical child process, complete with the same file descriptors.
  2. exec() — That child process then replaces its own code with the program you asked for (say, Node.js).

#3.2 The Swap Happens Before Your Program Even Starts

Here's the trick: right before exec() runs, the shell can quietly swap out where those tickets point.

So when you type:

terminal
Bash
node script.js > output.log

the shell clones itself, unplugs the child's FD 1 (stdout) from the screen, and plugs it into output.log instead. Then it hands control over to Node.js. Node never asked for this, never saw a config flag change, and has no idea anything is different — it just keeps writing to FD 1 like always.

// TIP
That single trick — swapping file descriptors before exec() — is the entire mechanism behind >, <, 2>&1, and pipes (|). Once it clicks, redirection stops feeling like magic.

#4. Streams in Practice: A TypeScript Example

#4.1 Console Methods and Their Underlying Streams

When you call console.log() in JavaScript or TypeScript, under the hood you're writing to FD 1 (stdout). When you call console.error(), you're writing to FD 2 (stderr).

#4.2 Writing to Streams Directly

If you're building a serious CLI tool, it's worth interacting with these streams directly rather than relying only on console:

stream-test.ts
TypeScript
// Run with: npx ts-node stream-test.ts

// Prompt the user on stdout (FD 1)
process.stdout.write("Please enter a username: ");

// Listen for data on stdin (FD 0)
process.stdin.on("data", (data: Buffer) => {
  const userInput = data.toString().trim();

  if (!userInput) {
    // Write to stderr (FD 2)
    // Even if stdout is redirected to a file, this error
    // will still show up on the user's screen!
    process.stderr.write("ERROR: Username cannot be empty!\n");
    process.exit(1);
  }

  // Write success to stdout (FD 1)
  process.stdout.write(`SUCCESS: Welcome to the system, ${userInput}.\n`);
  process.exit(0);
});

#4.3 Why Separating Streams Matters

Because stdout and stderr are separate channels, tools downstream can treat them differently. For example, you could pipe this script's stdout into a database while letting stderr trigger a monitoring alert — two different destinations, one script, zero no code changes required.


#5. Pro Tips and Common Gotchas

Once you start chaining terminal commands together, streams can get tricky fast. Here are a few survival tips most developers learn the hard way.

#5.1 Order Matters When Combining Streams

It's common to see 2>&1 used to merge stderr (2) into stdout (1) so both end up in the same file. But get the order wrong, and it fails silently.

// IMPORTANT
2>&1 is a snapshot taken at that exact moment — not a permanent link. Where you place it in the command changes everything.
  • ✅ Correct: command > output.log 2>&1
    ("Point stdout to output.log, then point stderr wherever stdout is currently pointing.")
  • ❌ Wrong: command 2>&1 > output.log
    ("Point stderr to the screen — because that's where stdout is right now — and only then redirect stdout to the file. Your errors will still leak onto the screen!")

#5.2 `tee`: Viewing and Saving Output at the Same Time

Want your output saved to a log file and visible live on screen? Use tee — it acts like a T-junction in your plumbing:

Bash
node script.js | tee output.log

Want errors captured too? Combine it with what you just learned:

Bash
node script.js 2>&1 | tee output.log

#5.3 Silencing Noise with `/dev/null`

/dev/null is a special file that acts like a digital black hole — anything written to it disappears instantly. It's perfect for suppressing output you don't care about:

Bash
# Discard error messages, keep normal output
command 2> /dev/null

# Discard everything
command > /dev/null 2>&1

#5.4 Watch Out for Buffering

If you pipe data between commands (e.g., node script.js | grep "SUCCESS"), you might notice your terminal seems to hang, then suddenly dumps a huge block of output at once.

// WARNING
Standard libraries often hold data in small chunks (commonly 4KB) when they detect they're not writing directly to a screen, then flush it all at once. If a pipeline ever feels "stuck," buffering is usually why.

If you want a script to feel "live" over a pipe, you may need to force a flush manually. Node's process.stdout usually handles this well on its own, but it's worth keeping in mind.

🔍 Click to expand: Forcing unbuffered output across common tools

Different tools expose different ways to disable buffering when you need real-time output:

Bash
# Python: force unbuffered stdout
python3 -u script.py

# stdbuf: override buffering for programs that don't expose a flag
stdbuf -oL -eL ./my-program | tee output.log

# Node.js: process.stdout is line-buffered when attached to a TTY,
# but fully buffered when piped — write() calls are usually fine as-is

These tricks matter most for long-running processes (log tailing, monitoring scripts, CI pipelines) where you want output as it happens, not in delayed batches.


#6. Quick Reference Cheat Sheet

Bookmark this for the next time you can't quite remember how to route your output:

Goal Command Syntax
Save standard output to a file command > file.log
Append output to an existing file command >> file.log
Save errors to a file command 2> errors.log
Save both output and errors to the same file command > all.log 2>&1
Save output and errors to separate files command > out.log 2> err.log
Hide all errors command 2> /dev/null
Hide absolutely everything command > /dev/null 2>&1
Show output live and save it command | tee output.log
Feed a file in as input command < input.txt

#Have You Got the Basics Down?

  • Understand what a file descriptor is
  • Know the default destination of stdin, stdout, and stderr
  • Understand why 2>&1 order matters
  • Know how to use tee to view and save output simultaneously
  • Try writing your own CLI tool using process.stdin and process.stdout
  • Experiment with stdbuf or -u flags on a long-running script

#7. A Closing Thought

"Everything is a file — and if you can read from it or write to it, Unix doesn't much care what it actually is."

— A paraphrase of the core Unix design philosophy

Once you internalize that streams are really just numbered tickets the shell quietly swaps around before your program even starts running, the terminal stops feeling like arcane magic. It becomes exactly what it is: a brilliantly elegant piece of 1970s plumbing that's still quietly running the internet today.

END OF ARTICLE
TAGS: #bash #beginners #cli #command-line #linux #nodejs #terminal #typescript
// ARTICLE_TELEMETRY & FEEDBACK

Enjoyed this architecture breakdown?

Leave a reaction or join the discussion below.

[ AUTHOR PROFILE ] // ENGINEERING & WRITING
View All Articles →
Fi Amanillah

Full Stack Developer

Software Engineer & Technology Writer exploring distributed systems, modern web architectures, and full-stack cloud engineering.

// DISCUSSION_THREAD0 COMMENTS

Community Insights & Architectural Q&A

Sort:
// JOIN_DISCUSSION

Share your architectural insights & feedback

Sign in with your account or continue as a guest reader to post questions and architectural perspectives.

// ACTIVE_THREAD_STREAM (0 threads)Showing 0 of 0
CONTINUE

TECH_DISPATCH // INSIGHTS

Enjoyed this Article?

Get notified as soon as new tech deep dives, AI analyses, semiconductor market stories, and engineering guides are published.

Zero spam. Unsubscribe anytime with 1-click.