JavaRush /Courses /Go SELF /stdout vs stderr and clean usage

stdout vs stderr and clean usage

Go SELF
Level 48, Lesson 3
Available

1. Why separate stdout and stderr

When you write a console program, it’s very easy to think: “Well, I’m printing to the console anyway—what’s the difference where exactly?” But the operating system, and most CLI tools in the world, have a good old agreement: normal data goes to stdout, and diagnostics go to stderr.

This isn’t philosophy or aesthetics. It’s a practical thing that makes pipelines like tool | other-tool and redirection > file work. If you mix everything into one stream, the user won’t be able to separate the “result” from “oops, something went wrong.”

Imagine your program can print a task list, and the user does this:

todo list > tasks.txt

They expect tasks.txt to contain only the list of tasks. If you also print warning: config not found or usage: ..., the file becomes garbage. And the most annoying part: your program “seems to work,” but scripts around it break.

At the Go standard library level, this is expressed directly in the API: os.Stdout and os.Stderr are different values. In CLI utilities, this is a historically important separation: stdout is the “product of the command,” stderr is “diagnostics and hints.”

2. What to write to stdout and what to stderr

While you’re learning, it helps to keep a simple rule in mind: if it’s data that can be processed further—print it to stdout; if it’s a message for a human about a problem or a hint—print it to stderr.

To avoid turning this into an argument like “is this message really data or not,” rely on common sense: imagine the user redirected stdout to a file or to another program. Would you want this message to end up inside the file? If not, then it belongs in stderr.

Below is a small table that usually answers 95% of questions:

What we print Where Why
Command result (list, calculations, JSON, table)
stdout
This is the “product” of the command
Errors (invalid arguments, failed to open a file)
stderr
This is diagnostics; it must not spoil the result
usage, help
stderr
This is a hint/diagnostics, not data
Progress, warnings
stderr
So stdout stays clean
Debug fmt.Println("debug")
stderr
So you don’t accidentally “break” the output

Note: about usage I wrote “in this course.” In the real world, you’ll see variations, but here we lock in a simple and predictable rule: print usage to stderr, so it doesn’t get mixed with data.

3. How to print to the right stream in Go

If you print via fmt.Println, you print to the “default” stream (usually stdout). For a disciplined CLI, we need to be able to print explicitly.

In Go this is straightforward: the function family fmt.Fprint/fmt.Fprintln/fmt.Fprintf takes the destination as the first argument—i.e., an io.Writer (in our case os.Stdout or os.Stderr).

Mini example: result to stdout, error to stderr

This is one of those moments when you add two lines to a program—and suddenly you become “the person who writes proper CLIs.” The example below is intentionally tiny so you see the idea and don’t drown in logic.

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Fprintln(os.Stdout, "result: OK")         // result: OK
	fmt.Fprintln(os.Stderr, "warning: something") // warning: something
}

If you run the program in a terminal, you’ll see both lines. But then the automation magic begins: the user can redirect stdout separately from stderr.

4. Clean usage in Go via flag

usage (or “usage help”) is a short text that explains how to run the program: which flags exist, which arguments are required, which values are allowed. Ideally, usage helps the user fix the error in one attempt, instead of playing “guess what the author meant.”

The most common beginner mistake: write usage manually once, then change flags and forget to update the help. As a result, the program says: unknown flag -p, while usage still promises -p. The user starts suspecting a conspiracy. And honestly, they’re right.

In Go, the flag package specifically helps you avoid duplicating information. Each flag has a name, a default, and a description—and from that you can print the flag list automatically. That’s why good usage is usually a “header” + flag.PrintDefaults().

flag.Usage and flag.PrintDefaults()

In the flag package there is a variable flag.Usage. It is a function that the package calls when it needs to show usage. By default it already prints something, but usually we want a clearer and more stable format—and we want to control the stream (almost always stderr).

Example: minimal usage for todo

Let’s agree: we’re building a small CLI app todo, which for now can accept -title (required) and -priority (optional). No subcommands and no complicated architecture—only what we need for the stdout/stderr/usage topic.

package main

import (
	"flag"
	"fmt"
	"os"
)

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s -title <text> [-priority <n>]\n", os.Args[0])
		flag.PrintDefaults()
	}

	_ = flag.String("title", "", "task title (required)")
	_ = flag.Int("priority", 1, "priority 1..5")
	flag.Parse()
}

Note three things.

  • We print usage to os.Stderr, because it is help/diagnostics.
  • We use os.Args[0] so that usage shows the program name “as it was invoked.”
  • We don’t print the flag list by hand—flag.PrintDefaults() does it for us.

Message order on error

One of the most annoying things in a CLI is when the program just dumps usage without explaining what exactly is wrong. The user sees a wall of text and doesn’t know what to fix: wrong flag? missing argument? number out of range? invalid string format?

Good UX for basic input errors is usually: one short line “what’s wrong”, then (optionally) an empty line, and then usage.

Important note: we’re not discussing exit codes here (that will be a separate topic). So in the example below we simply return from main.

Example: required -title and usage on error

The code will become a couple of lines longer, but this is exactly the skeleton you’ll later copy from project to project.

package main

import (
	"flag"
	"fmt"
	"os"
)

func main() {
	title := flag.String("title", "", "task title (required)")
	flag.Parse()

	if *title == "" {
		fmt.Fprintln(os.Stderr, "error: missing -title")
		flag.Usage()
		return
	}

	fmt.Fprintln(os.Stdout, "added:", *title) // added: Buy milk
}

Here the result (added: ...) goes to stdout, and the error and usage go to stderr. And this is exactly “the right way” for a basic CLI.

Why we print usage to stderr

Sometimes a philosophical question comes up: “What if the user called -h? That’s not an error, so why stderr?” Different utilities do it differently, and in some cases help is indeed printed to stdout.

But at the start of the course, something else matters more: predictability and a ‘clean stdout’. If stdout always contains only command data, then anyone can safely do todo ... > out.txt and not worry that help text will suddenly end up there.

Also, help is text for the user, not data for processing. And stderr is specifically intended for “messages about what’s happening” (errors, warnings, explanations).

So for this course the agreement is: print usage/help to stderr. Even if you decide to do it differently in the future, you’ll do it consciously, not accidentally.

Where flag prints errors by itself

The flag package can complain on its own if you pass an unknown flag or an invalid value. For example, -priority=abc for an Int is a parse error. And it’s important to understand that some messages may be printed by flag itself (before your code even reaches the semantic validation).

In the basic variant this is fine: flag prints its messages to the standard error stream. When you override flag.Usage, you at least make the help consistent.

5. Control flow and an example

It helps to see a “picture” so your brain stops perceiving this as a set of random Fprintln calls. When a user runs a CLI, you almost always have two branches: everything is OK—print data; something is wrong—print diagnostics.

Mini diagram

flowchart TD
    A[Program start] --> B[Parse args / flag.Parse]
    B --> C{Validation passed?}
    C -->|yes| D[Print result to stdout]
    C -->|no| E[Print reason to stderr]
    E --> F[Print usage to stderr]

The idea is simple: stdout stays “clean” and contains only the result of the successful path.

Mini example: adding a task

Let’s put together a small but complete example. It’s not “perfect” and not “architectural,” but it shows stream discipline and printing usage.

package main

import (
	"flag"
	"fmt"
	"os"
)

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s -title <text> [-priority 1..5]\n", os.Args[0])
		flag.PrintDefaults()
	}

	title := flag.String("title", "", "task title (required)")
	priority := flag.Int("priority", 1, "priority 1..5")
	flag.Parse()

	if *title == "" {
		fmt.Fprintln(os.Stderr, "error: missing -title")
		flag.Usage()
		return
	}
	if *priority < 1 || *priority > 5 {
		fmt.Fprintln(os.Stderr, "error: -priority must be in range 1..5")
		flag.Usage()
		return
	}

	fmt.Fprintf(os.Stdout, "added: %s (p=%d)\n", *title, *priority) // added: Buy milk (p=2)
}

What matters here is discipline: errors and help do not contaminate the result.

6. Common mistakes

Mistake #1: printing errors via fmt.Println, i.e., to stdout.
This happens by inertia: you’re already used to fmt.Println, and your hand automatically uses it everywhere. As a result, the user redirects output to a file, and it ends up containing error: ... and Usage: .... The fix is simple: print any errors and help via fmt.Fprintln(os.Stderr, ...), and print data via os.Stdout.

Mistake #2: printing usage without a specific reason.
When a program simply prints help and silently exits, the user doesn’t understand what exactly they did wrong. Good style is to first explain the reason in one line (for example, missing -title or -priority must be in range 1..5), and only then show usage.

Mistake #3: writing usage by hand and forgetting to update it when flags change.
Today you have -priority, tomorrow you rename it to -p, but usage stays old. The user sees the contradiction and starts doubting reality. The solution is to use flag.PrintDefaults(): then the flag list in help automatically matches the declarations in code.

Mistake #4: mixing normal output and help text.
If in one place you print Error: ..., in another ERROR - ..., and in a third usage: without a capital letter, it feels like the program was written by three people and one cat. Pick a single style: for example, error: ... (lowercase) and then Usage: .... Even in a tiny utility, this makes the interface calmer.

Mistake #5: hardcoding the program name in usage.
If you write Usage: todo ..., and then the binary is named differently (or the user runs ./todo), the hint looks strange. It’s better to use os.Args[0]: then usage shows the name as it is actually invoked, and it confuses people less.

1
Task
Go SELF, level 48, lesson 3
Locked
Separate output
Separate output
1
Task
Go SELF, level 48, lesson 3
Locked
Polite usage
Polite usage
1
Task
Go SELF, level 48, lesson 3
Locked
Clean calculator
Clean calculator
1
Task
Go SELF, level 48, lesson 3
Locked
Stream filter
Stream filter
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION