Error handling
Error Code Format
Section titled “Error Code Format”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| Part | Format | Example | Description |
|---|---|---|---|
| Prefix | X | X | Always X for errors |
| HTTP | 3 digits | 404 | HTTP status code |
| Sequence | 3 digits | 003 | Module-specific sequence |
| Mnemonic | 6+ chars | NODNOF | Uppercase identifier |
Error Definition
Section titled “Error Definition”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:
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.Specis drift (tracked in the drift register)
Message formatting rules
Section titled “Message formatting rules”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)."
Error Usage
Section titled “Error Usage”// Return with arguments matching msg placeholdersreturn X404908STKNFD("my-stack")// Message: "stack my-stack not found"
return X400002VALINP("missing required field name")// Message: "invalid input missing required field name"HTTP Status Ranges
Section titled “HTTP Status Ranges”| Range | Meaning | Use Case |
|---|---|---|
| 400-499 | Client errors | Bad input, not found, auth |
| 500-599 | Server errors | Internal failures, timeouts |
| 503 | Service unavailable | Quorum loss, maintenance |
Choosing a Sequence Number
Section titled “Choosing a Sequence Number”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:
| Module | Example | Message template |
|---|---|---|
| core (global) | X400902OPABTD | operation aborted |
| app | X404002APPNOF | application {name} not found |
| stack | X404908STKNFD | stack {stack_name} not found |
| cluster | X503020QURLST | cluster quorum lost, {voters_available}/{voters_required} voters available |
| storage | X404914STGNOF | storage provider {name} not found |
| backup | X404400BKPNOF | backup {backup_id} not found |
| vault | X200905VLTLCK | vault locked |
Event Code Format
Section titled “Event Code Format”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)Event Prefixes
Section titled “Event Prefixes”| Prefix | Category | Example |
|---|---|---|
| V | Lifecycle/info event | V200300CTRSTR (container started) |
| A | Audit event | A200100STKINS (stack installed) |
| X | Coded error or warning fanned onto the bus | X500907STKFAL (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 Definition
Section titled “Event Definition”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:
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)Error vs Audit vs Event
Section titled “Error vs Audit vs Event”| Aspect | Error (X) | Audit (A) | Event (V) |
|---|---|---|---|
| Purpose | Signal failure, abort flow | Track API operations | Track internal operations |
| Criteria | Exceptional condition | Has API + mutates state | No API (internal only) |
| Control flow | Returns error | Continues execution | Continues execution |
| Persistence | Logged only | Persisted to audit DB (hash-chained) | Logged only |
| Handlers | Error handlers | Event listeners via queue | Event listeners via queue |
See audit module for the
audit persistence contract.
Best Practices
Section titled “Best Practices”- Use specific error types — never return generic
errors.New() - Include context — use placeholders like
{name},{reason} - Fail fast — validate early, return immediately on invalid input
- Use the typed logging API —
corelog.Warn(ctx, <coded>)/corelog.Error(ctx, <coded>, cause); rawslog.Warn/slog.Errorare lint-banned in production paths - No backward compatibility aliases — use proper X-coded names