Skip to content

ADR-046: Emit a JSON error envelope on the failure path

When a command fails under --json or --yaml, stdout carries the document with error in place of data: kind, message and exitCode from the ADR-011 taxonomy, and details when there are any (ADR-075). The human-readable line still goes to stderr, and the exit code is the kind's. A document holds data or error, never both, so a consumer tells success from failure by which key is present. Under --dry-run a failure that is a verdict on the real run is the answer rather than a failure, and goes inside the preview; only a failure to reach a verdict is a top-level error (ADR-096). Under --describe a path that names no command is answered inside description (ADR-097). The failure shape does not vary by command, so it is published once, as docs/reference/schemas/output/output.error.schema.json.

Machine output that was asked for counts even when parsing failed before the flag was read: --json, --yaml, --dry-run and --describe are recovered from the raw arguments, because an unknown flag or command is when a script most needs a parseable answer. A malformed invocation is validation, exit 2: an unknown flag, command or subcommand, a bad flag value, a missing required flag, a broken flag group, or the wrong number of arguments. It is the caller's mistake, not internal.

Write a failure through jsonoutput.WriteError, never as a bare string on stdout, and keep the human-readable line on stderr. When adding an error kind to internal/domain/errors, add it to Kinds(). The published schema and --describe take their enum from it, and tests validate real emitted envelopes against the schema, so a kind missing there publishes a contract the CLI can break. The envelope reports exitCode; it does not decide it. Route an error from cmd/bb through cli.ClassifyUsageError, which maps pflag's typed errors and Cobra's usage errors onto validation and leaves every other error's kind alone. Cobra raises its own usage errors as plain fmt.Errorf values, so recognising them means matching their text. Do not add a marker to cobraUsageErrorMarkers without a case in TestClassifyUsageErrorMatchesCobrasRealMessages that drives the real condition through a command tree, so a Cobra upgrade that rewords a message fails the build instead of reclassifying malformed invocations as internal.

Machine output is read by agents and CI scripts. Empty stdout beside an unparseable line on stderr cannot be told from malformed output, and cannot be branched on by kind. Carrying error where data would sit keeps the document self-describing without a discriminator field, and lets the success schema stay closed. A typo reported as internal tells a consumer the CLI broke, so an agent branching on kind would retry or escalate a failure it should fix in its own invocation.

Not chosen

  • Write the error envelope to stderr instead of stdout: Leaves stdout empty, and mixes the envelope into the diagnostics stderr carries.
  • Add error alongside a null data: Requires the success schema to allow a new property and to relax data, and makes a command whose successful data is legitimately null indistinguishable from a failure without also checking error for null. Presence of one key or the other is unambiguous.
  • A failure schema per command: Per-command copies would be identical files that drift, and one schema lets a consumer validate an error from a command released after it was written.
  • Match Cobra usage errors by wrapping every command's Args and FlagErrorFunc: Structural rather than text-based, and appealing for that reason, but Cobra returns the unknown-command error from Find before any command-level hook runs, so it cannot be caught that way. A partial structural solution plus text matching for the remainder is more moving parts than text matching guarded by a test that drives the real conditions.