Skip to content

Limitations

What this package does not do, what it does not check, and the combinations that do not work. Each of these is current behaviour verified against the source, not a plan.

An outcome overrides the level and code the caller asked for

An error carrying an Outcome decides its own log level and exit code, whatever the reporting call said. Error(ctx, ErrRunSubCommand) logs at WARN, and a code attached with WithExitCode loses to the outcome's.

That is the point of an outcome rather than a defect — the error knows what kind of ending it is and the reporting site usually does not — but it does mean a caller cannot promote one of these to an error-level report without replacing the outcome.

Quietly is ignored by Error and Warn

Quietly is read on the Fatal path only. Error(ctx, err, Quietly()) logs at ERROR, and no error or warning tells you the option did nothing.

It exists for a termination whose whole meaning is the exit code; a non-terminal report has no code to carry the meaning instead.

A usage outcome's exit code cannot be chosen

The three sentinels this module ships carry ExitCodeUsage. There is no option to change the constant, and no way to opt a particular sentinel out of it. Attaching a different code to one of them does nothing — the outcome wins.

What you can do is build your own error with a different outcome: WithOutcome(err, Outcome{Code: 64, ...}).

Details are not debug-gated

Hints and details both appear in the err group at every level, debug or not. Details are a category of information, not a privacy boundary — if something must not reach an ordinary user, it must not be attached to the error.

Nothing is redacted

Messages, hints, details and attributes are passed to the logger verbatim. Do not attach a credential, a token or personal data to an error and expect the report to hide it.

Exit codes do not survive a rebuilt error

The exit-code wrapper is an unexported type. errors.Is, errors.As and errors.Join all see through it, and ExitCode finds a code through a join — but anything that reconstructs an error from err.Error(), such as an RPC boundary carrying only strings, drops it along with the hints and the stack.

Fatal does not exit, and nothing else does either

Fatal reports and returns the code it believes the process should use. Nothing in this module calls os.Exit.

That is a deliberate limitation on what the module will do for you: main owns termination, because a library that exits skips every deferred cleanup between itself and main. If nothing acts on the returned code, the process ends normally. See the worked pattern.

There is no output writer

All output goes through the *slog.Logger you supply. There is no writer option, no colour control, no format control and no way to send usage output and error output to different places from inside the handler.

To change where reports go, or how they look, construct a different slog.Handler. To send usage somewhere specific, do it in the closure you pass to SetUsage.

Fixed strings are not translatable

subcommand required, unknown subcommand, command not yet implemented and internal invariant violated are English literals in the source. There is no message catalogue and no hook to replace them.

A tool that needs localised output can override the text per report with an outcome's Message, or catch the condition before it reaches the handler.

One usage printer, no per-command registry

The handler stores a single func() error. There is no map from command to printer and no stack. Setting it once at the root means every parent command prints the root's usage; the fix is to set it in each command's pre-run, which is the caller's job.

The handler is not concurrency-safe to reconfigure

SetUsage writes a struct field with no lock, and every report reads Logger, Help, Exit and Usage unguarded. Reporting from several goroutines is fine. Calling SetUsage while another goroutine reports is a data race — the race detector will say so. Configure the handler before you start concurrent work.

Nil is not defended against

  • A nil *slog.Logger passed to New panics on the first report, not at construction.
  • A StandardErrorHandler built as a struct literal without a Logger does the same.

New with a non-nil logger is the supported construction path; the others are the consequence of the struct being exported.

No telemetry, no crash reporting, no panic recovery

The package logs and returns. It does not install a recover, does not build a crash report, and emits no metrics or traces. The dependency footprint is enforced by a guard test, so that is not going to change quietly.