Color Output & User-Friendly Errors
Operators judge CLIs by error clarity: what failed, what to fix next, and whether automation can detect failure class.
Go tools separate stdout (data contract) from stderr (logs, warnings, colored hints).
Libraries like fatih/color and lipgloss add ANSI colors when output is a terminal, and degrade gracefully in CI logs.
Summary
User-friendly errors start with wrapped error values, end with concise stderr messages, and finish with documented os.Exit codes.
Color highlights severity (red error, yellow warning, green success) without polluting JSON or TSV on stdout.
Disable styling when NO_COLOR is set or when stdout/stderr is not a TTY.
Recipe
Quick-reference recipe card - copy-paste ready.
func fail(err error) {
color.New(color.FgRed).Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
func main() {
if err := run(); err != nil {
fail(err)
}
}When to reach for this:
- Operator-facing tools where mistakes need actionable hints.
- Scripts that parse exit codes but show humans colored stderr.
- CLIs that emit JSON on stdout and prose errors on stderr.
Working Example
package main
import (
"errors"
"fmt"
"io"
"os"
"github.com/fatih/color"
)
var (
errConfig = errors.New("config file not found")
errAuth = errors.New("authentication failed")
)
const (
exitGeneral = 1
exitConfig = 2
exitAuth = 3
)
func usage(w io.Writer) {
fmt.Fprintln(w, "usage: mytool --config path.yaml")
}
func run(args []string) error {
if len(args) == 0 {
usage(os.Stderr)
return errConfig
}
return nil
}
func main() {
color.NoColor = os.Getenv("NO_COLOR") != ""
err := run(os.Args[1:])
if err == nil {
color.New(color.FgGreen).Fprintln(os.Stderr, "ok")
return
}
switch {
case errors.Is(err, errConfig):
color.New(color.FgYellow).Fprintln(os.Stderr, "hint: copy config.example.yaml to config.yaml")
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(exitConfig)
case errors.Is(err, errAuth):
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(exitAuth)
default:
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(exitGeneral)
}
}What this demonstrates:
- Sentinels (
errConfig,errAuth) map to distinct exit codes. - Yellow hint precedes the red error line for config problems.
NO_COLORdisables ANSI for accessibility and CI.- Success still prints to stderr so stdout stays empty for pipes.
Deep Dive
How It Works
fmt.Errorf("load %s: %w", path, err)wraps errors forerrors.Isanderrors.As.maintranslates errors to exit codes once; libraries returnerrorwithoutos.Exit.- Color packages check TTY capability; lipgloss can style whole blocks consistently.
- Structured logs (slog) can mirror stderr messages for long-running CLIs.
Stream Conventions
| Stream | Content | Examples |
|---|---|---|
| stdout | Machine-readable | JSON lines, IDs, kubectl compatible tables |
| stderr | Human-oriented | errors, warnings, progress, debug |
| exit code | Automation signal | 0 ok, 2 config, 3 auth |
Exit Code Guidelines
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General failure |
| 2 | Usage or config error |
| 3+ | Domain-specific (document in README) |
Go Notes
// slog for verbose mode on stderr
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))- Libraries must not call
colordirectly on behalf of callers; return errors upward. - For cobra, use
SilenceErrors: trueand print styled errors inmainafterExecute. - Test messages with
bytes.Bufferandcolor.NoColor = truefor stable assertions.
Gotchas
- Color on stdout - Breaks
tool | jqwhen ANSI leaks into JSON. Fix: color stderr only. - Printing usage on stdout - Pipes capture help text as data. Fix:
flag.Usageand cobra help target stderr. - Generic "something went wrong" - Operators cannot fix issues. Fix: include which flag, file, or host failed.
- os.Exit in libraries - Skips defers and prevents wrapping. Fix: exit only in
main. - Ignoring NO_COLOR - Accessibility and CI policies expect respect for the env var. Fix: check
NO_COLORand TTY before styling.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Plain fmt (no color) | CI-only tools, logs archived | Terminal UX is a product requirement |
| lipgloss | Consistent themes with bubbletea | One red error line suffices |
| cobra error templates | Already on cobra | Simple flag tools |
| log/slog only | Verbose diagnostic mode | Operators need copy-paste fixes |
FAQs
Should success messages use color?
Optional green checkmarks on stderr help humans.
Do not print success prose to stdout when the contract is JSON.
How do I preserve error chains in output?
Use %w and print err once at the top level.
Verbose mode can log fmt.Sprintf("%+v", err) with pkg/errors if imported.
What is NO_COLOR?
A convention: when set, disable ANSI color.
Honor it alongside TTY detection.
How do Windows consoles differ?
Modern Windows Terminal supports ANSI.
Older conhost may need explicit color.NoColor = true; test release binaries on target shells.
Can I colorize JSON?
Avoid coloring stdout JSON.
Pretty-print for humans belongs in a --format table flag or stderr summary.
How does cobra print errors?
Execute prints returned errors to stderr by default.
Set SilenceErrors to apply custom styling in main.
Should warnings use exit code 0?
Yes for non-fatal warnings.
Use exit 1+ only when the operation failed or partial success is unacceptable.
How do I test exit codes?
Use os/exec to run the compiled binary in tests, or factor logic into run() error and test errors without exiting.
When is slog better than color?
When operators need timestamps and levels in long runs.
Pair slog for --verbose and keep default errors one line.
How do friendly errors relate to logging services?
CLIs surface immediate fixes; services log the same wrapped errors with request IDs.
Share sentinel error types across cmd and internal packages.
Related
- CLI Tools Basics - stderr usage and exit codes
- flag Package & POSIX-Style Flags - usage on stderr
- bubbletea Terminal UI & Progress Bars - lipgloss styling
- cobra & urfave/cli Command Trees - SilenceErrors pattern
- CLI Tools Best Practices - testing and UX checklist
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).