Skip to content
SHC Docs

Error handling

Errors use the format X{HTTP}{SEQ}{MNEMONIC} — 13 characters canonically, with a 6-or-more-letter mnemonic tail:

X404003NODNOF
│└┬┘└┬┘└──┬──┘
│ │ │ └── 6+ char uppercase mnemonic (NODNOF = node not found)
│ │ └── 3-digit sequence number (003 = unique within module)
│ └── 3-digit HTTP status (404 = not found)
└── X prefix for errors
PartFormatExampleDescription
PrefixXXAlways X for errors
HTTP3 digits404HTTP status code
Sequence3 digits003Module-specific sequence
Mnemonic6+ charsNODNOFUppercase identifier

Errors are defined as factory functions returning *coded.CodedError, paired with a coded.Spec entry the module registers at init() time. Real example from modules/stack/errors.go:

modules/stack/errors.go
package stack
import "gitlab.com/bitspur/selfhosted-cloud/ce/core/coded"
// X404908STKNFD is raised when the requested stack does not exist
// for the resolved (tenant, environment) pair.
func X404908STKNFD(stackName string) *coded.CodedError {
return coded.New("X404908STKNFD",
"stack {stack_name} not found",
map[string]any{"stack_name": stackName})
}
// stackErrorSpecs is the static metadata surfaced to the registry.
// Keep it in lockstep with the factories above.
var stackErrorSpecs = []coded.Spec{
{
Code: "X404908STKNFD",
Template: "stack {stack_name} not found",
Params: []string{"stack_name"},
},
}
func init() {
for _, s := range stackErrorSpecs {
coded.Register(s)
}
}

Key points:

  • Function name IS the error code
  • Message uses {placeholder} syntax
  • HTTP status derived from chars 2-4 of code
  • Context passed as map[string]any
  • Every factory’s code MUST also appear in the module’s spec slice — a factory without a registered coded.Spec is drift (tracked in the drift register)

Enforced by scripts/arch-fitness.sh check 8 and the output contract §7: a template is one lowercase flat clause.

  • Lower-case, imperative, one line, no trailing period.
  • No colons (:) or semicolons (;) — separate clauses with a comma or whitespace.
  • No parentheses (( / )) — put qualifiers inline with commas.
  • Use {placeholder} for values, no quotes around them.
  • Backticks are allowed for literal identifiers (commands, paths).

Good: "local node not initialized, {path} missing, run `shc init` first" Bad: "local node not initialized: {path} missing (run shc init first)."

// Return with arguments matching msg placeholders
return X404908STKNFD("my-stack")
// Message: "stack my-stack not found"
return X400002VALINP("missing required field name")
// Message: "invalid input missing required field name"
RangeMeaningUse Case
400-499Client errorsBad input, not found, auth
500-599Server errorsInternal failures, timeouts
503Service unavailableQuorum loss, maintenance

The middle 3 digits are a module-chosen sequence, unique within an HTTP class. There is no reserved per-module range — pick a numeric prefix (X<HTTP><SEQ>) no other code uses. coded.Register panics on an exact duplicate code, and scripts/arch-fitness.sh check 8b ratchets numeric-prefix collisions (same X<HTTP><SEQ> under two mnemonics) — the grandfathered count may only fall, so a new collision fails make lint. The full inventory lives in docs/reference/errors.md (generated).

Real examples per module:

ModuleExampleMessage template
core (global)X400902OPABTDoperation aborted
appX404002APPNOFapplication {name} not found
stackX404908STKNFDstack {stack_name} not found
clusterX503020QURLSTcluster quorum lost, {voters_available}/{voters_required} voters available
storageX404914STGNOFstorage provider {name} not found
backupX404400BKPNOFbackup {backup_id} not found
vaultX200905VLTLCKvault locked

Events use a similar format with different prefixes indicating the event category:

V200300CTRSTR
│└┬┘└┬┘└──┬──┘
│ │ │ └── 6+ char mnemonic
│ │ └── 3-digit sequence
│ └── 3-digit HTTP-style status (2xx = success, 5xx = error)
└── Event prefix (V or A)
PrefixCategoryExample
VLifecycle/info eventV200300CTRSTR (container started)
AAudit eventA200100STKINS (stack installed)
XCoded error or warning fanned onto the busX500907STKFAL (stack operation failed)

There are no other event prefixes. Errors and warnings reach the bus as their X-codes via the typed logger (corelog.Warn/Error), with IsError/IsWarning set from the code’s HTTP class.

Event types are string constants plus a registry.EventType entry with a payload struct — not coded-error factories. Real example from modules/app/events.go:

modules/mymodule/events.go
package mymodule
import "gitlab.com/bitspur/selfhosted-cloud/ce/core/registry"
const (
// A200100STKINS — stack installed.
EventStackInstalled = "A200100STKINS"
)
var moduleEventTypes = []registry.EventType{
{Name: EventStackInstalled, Payload: StackEvent{}},
}
// register.go init() surfaces the slice to the manifest checker:
// registry.EventTypes(ModuleName, moduleEventTypes)
AspectError (X)Audit (A)Event (V)
PurposeSignal failure, abort flowTrack API operationsTrack internal operations
CriteriaExceptional conditionHas API + mutates stateNo API (internal only)
Control flowReturns errorContinues executionContinues execution
PersistenceLogged onlyPersisted to audit DB (hash-chained)Logged only
HandlersError handlersEvent listeners via queueEvent listeners via queue

See audit module for the audit persistence contract.


  1. Use specific error types — never return generic errors.New()
  2. Include context — use placeholders like {name}, {reason}
  3. Fail fast — validate early, return immediately on invalid input
  4. Use the typed logging APIcorelog.Warn(ctx, <coded>) / corelog.Error(ctx, <coded>, cause); raw slog.Warn/slog.Error are lint-banned in production paths
  5. No backward compatibility aliases — use proper X-coded names