---
title: "20.3 stdin, stdout, and stderr"
description: "The three standard streams every program has, piping between programs, why errors belong on stderr, and exit codes."
url: "https://learnrust.net/chapter-20/stdin-stdout-stderr/"
last_updated: "2026-06-13"
---

# 20.3 stdin, stdout, and stderr

Every command-line program is born connected to three streams, whether it uses them or not. **Standard input** (stdin) is where input arrives, **standard output** (stdout) is where normal results go, and **standard error** (stderr) is where problems are reported. You've already used the first two without naming them: `read_line` reads from stdin, `println!` writes to stdout. This lesson names the third, explains why keeping output and errors on separate streams matters, and shows how these streams let small tools combine into big ones.

## The streams you already use

`println!` writes to stdout. `read_line` reads a line from stdin. That much is review from chapter 1. What's new is the *why*: these are separate, redirectable channels, not just "the screen" and "the keyboard." When you run a program in a terminal, all three are wired to the terminal by default, which is why output appears and typing works. But each can be redirected independently, and that's the whole point of having three.

Reading all of stdin (useful when your tool is meant to process piped-in text rather than a named file) looks like this:

```rust
use std::io::{self, Read};

fn main() {
    let mut input = String::new();
    io::stdin().read_to_string(&mut input).expect("failed to read stdin");

    let line_count = input.lines().count();
    println!("{line_count} lines");
}
```

`io::stdin()` is a handle to the standard input stream; `read_to_string` drains all of it into a `String` (the same method name as the file version last lesson, because both are just "a source of bytes"). Then `input.lines().count()` is plain chapter-19 iteration. Run with input piped in:

```bash
$ printf 'a\nb\nc\n' | cargo run
```

```
3 lines
```

The `|` there is a **pipe**: it connects the stdout of the program on the left (`printf`) to the stdin of the program on the right (your tool). That connection is the foundation of the Unix command-line philosophy, small programs each doing one thing, strung together with pipes. Your tool just read its input; it neither knew nor cared that the input came from another program rather than the keyboard.

## Why errors go on a separate stream

Here's the design decision that confuses newcomers and then, once it clicks, never does. Normal output goes to stdout. Error messages and diagnostics go to **stderr**. Two streams for what might seem like one job. The reason is composability.

Imagine a tool that searches a file and prints matching lines. Someone pipes its output into another program, or redirects it into a file: `mytool query data.txt > results.txt`. They want `results.txt` to contain *only the matches*, the actual results, not "Searching data.txt..." or "Warning: 3 lines skipped." If those status messages went to stdout, they'd land in `results.txt`, corrupting the data. By putting results on stdout and everything else on stderr, the two never mix: the `>` redirect captures stdout (clean results) while stderr still prints to the terminal where the human can see it.

In Rust, stdout is `println!` and stderr is **`eprintln!`** (and `eprint!`), introduced for debugging back in lesson [3.4](https://learnrust.net/chapter-3/print-debugging/). Now you see its real job:

```rust
fn main() {
    let input = "12\nnot a number\n7";

    for line in input.lines() {
        match line.parse::<i32>() {
            Ok(n) => println!("{}", n * 2),                  // result -> stdout
            Err(_) => eprintln!("skipping invalid: {line}"), // diagnostic -> stderr
        }
    }
}
```

Run normally, both streams show in the terminal interleaved:

```
24
skipping invalid: not a number
14
```

But redirect stdout to a file (`cargo run > out.txt`), and only the results land there:

```
out.txt:
24
14
```

while `skipping invalid: not a number` still prints to the terminal, because it went to stderr, which wasn't redirected. The diagnostic reached the human without polluting the data. That separation is why `eprintln!` exists, and why every well-behaved tool sends results to stdout and everything else to stderr.

> **Best practice**
>
> Send your program's actual output, the data a user or another program wants, to stdout with `println!`/`print!`. Send everything else, progress messages, warnings, errors, usage text, to stderr with `eprintln!`/`eprint!`. This keeps your tool pipeable: redirecting or piping stdout captures clean results, while diagnostics still reach the terminal. Mixing the two is the most common way a tool becomes painful to compose with others.

## Exit codes

The other half of a tool's conversation with the outside world is its **exit code**: a small integer the program returns when it ends, by convention `0` for success and any non-zero value for failure. The shell and other programs read it to decide what to do next (this is what `&&` in `build && test` checks, run the second command only if the first exited zero). You met `echo $?` and exit codes back in lesson [7.8](https://learnrust.net/chapter-7/halting-your-program-early/); here they're part of being a good command-line citizen.

The cleanest way to set the exit code is to have `main` return a `Result`. If `main` returns `Ok`, the process exits `0`; if it returns `Err`, Rust prints the error to *stderr* (correctly) and exits with a non-zero code:

```rust
use std::fs;

fn main() -> Result<(), std::io::Error> {
    let contents = fs::read_to_string("config.txt")?;
    println!("{} bytes of config", contents.len());
    Ok(())
}
```

If `config.txt` exists, it prints the size and exits `0`. If it doesn't, the `?` propagates the error, Rust prints something like `Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }` to stderr, and the process exits non-zero, all automatically. This is the lesson-[12.3](https://learnrust.net/chapter-12/propagating-errors-question-mark-operator/) `main`-returns-`Result` pattern doing real work: clean exit codes and error-to-stderr for free, no manual `process::exit` needed.

> **Key insight**
>
> A command-line program's interface to the rest of the system is four channels: arguments and stdin coming in, stdout and stderr going out, and an exit code on the way out the door. A tool that respects all four, reads its input, writes results to stdout, writes diagnostics to stderr, and exits zero on success, drops cleanly into pipelines and scripts alongside every other Unix tool. The project at the end of this chapter is built to honor exactly these conventions.

## Quiz time

**Question #1**

What's the difference between stdout and stderr, and why have both?

<details class="solution">
<summary>Show solution</summary>

stdout carries the program's actual output (the results); stderr carries diagnostics, warnings, errors, and progress messages. They're separate so a user can redirect or pipe stdout (capturing clean results into a file or another program) while diagnostics still print to the terminal. If both shared one stream, status messages would contaminate the data whenever output was redirected. In Rust, stdout is `println!` and stderr is `eprintln!`.

</details>

**Question #2**

A tool prints results with `println!` and warnings with `println!` too. A user runs `tool input > out.txt` and complains that `out.txt` has warning text mixed into the data. What's the fix?

<details class="solution">
<summary>Show solution</summary>

Print the warnings with `eprintln!` instead of `println!`, so they go to stderr. The `>` redirect only captures stdout, so with warnings on stderr, `out.txt` would contain only the results while the warnings still appear in the terminal. The bug is that diagnostics were being written to stdout, where the redirect swept them into the file.

</details>

**Question #3**

How does returning a `Result` from `main` set the program's exit code, and where does the error text go?

<details class="solution">
<summary>Show solution</summary>

If `main` returns `Ok(())`, the process exits with code `0` (success). If it returns `Err(e)`, Rust prints the error (via its `Debug` form) to *stderr* and exits with a non-zero code, automatically. This gives you correct exit codes and error-on-stderr behavior for free, just propagate errors with `?` up to `main`, with no manual `process::exit` call (the lesson-12.3 pattern).

</details>

Arguments, files, and the standard streams are configured at the moment a program runs. The last input source, the next lesson ([20.4](https://learnrust.net/chapter-20/environment-variables/)), comes from the surrounding environment: environment variables, the standard way to configure a tool's behavior without cluttering its arguments.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
