Color Output & User-Friendly Errors
Operators judge CLIs by error clarity: what failed, what to fix next, and whether automation can detect failure class.
Search across all documentation pages
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.
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.
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:
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:
errConfig, errAuth) map to distinct exit codes.NO_COLOR disables ANSI for accessibility and CI.fmt.Errorf("load %s: %w", path, err) wraps errors for errors.Is and errors.As.main translates errors to exit codes once; libraries return error without os.Exit.| 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 |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General failure |
| 2 | Usage or config error |
| 3+ | Domain-specific (document in README) |
// slog for verbose mode on stderr
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))color directly on behalf of callers; return errors upward.SilenceErrors: true and print styled errors in main after Execute.bytes.Buffer and color.NoColor = true for stable assertions.tool | jq when ANSI leaks into JSON. Fix: color stderr only.flag.Usage and cobra help target stderr.main.NO_COLOR and TTY before styling.
| 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 |
Optional green checkmarks on stderr help humans.
Do not print success prose to stdout when the contract is JSON.
Use %w and print err once at the top level.
Verbose mode can log fmt.Sprintf("%+v", err) with pkg/errors if imported.
A convention: when set, disable ANSI color.
Honor it alongside TTY detection.
Modern Windows Terminal supports ANSI.
Older conhost may need explicit color.NoColor = true; test release binaries on target shells.
Avoid coloring stdout JSON.
Pretty-print for humans belongs in a --format table flag or stderr summary.
Execute prints returned errors to stderr by default.
Set SilenceErrors to apply custom styling in main.
Yes for non-fatal warnings.
Use exit 1+ only when the operation failed or partial success is unacceptable.
Use os/exec to run the compiled binary in tests, or factor logic into run() error and test errors without exiting.
When operators need timestamps and levels in long runs.
Pair slog for --verbose and keep default errors one line.
CLIs surface immediate fixes; services log the same wrapped errors with request IDs.
Share sentinel error types across cmd and internal packages.
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).
Reviewed by Chris St. John·Last updated Jul 19, 2026