cmderr provides a durable, serializable representation for asynchronous command errors in Go. It enables error category matching and concrete domain error reconstruction across process boundaries.
In distributed systems or asynchronous workflows (like background workers or CQRS), errors often need to be:
- Persisted to a database.
- Transmitted over the wire (JSON).
- Reconstructed in another process to maintain
errors.Isanderrors.Asfunctionality.
cmderr solves this by decoupling the durable error category (a string-based Code) from the in-memory concrete error type.
- ✅ Serializability: Built-in support for JSON marshaling/unmarshaling of error chains.
- ✅ Category Matching: Compare errors across process boundaries using string codes (
errors.Is). - ✅ Domain Error Reconstruction: Register codecs to automatically restore concrete domain error types when decoding (
errors.As). - ✅ Recursive Chains: Supports nested causes for complex failure tracking.
- ✅ Opinionated Normalization: Easily convert standard Go errors (including context timeouts/cancellations) into durable
CommandErrorinstances.
go get github.com/madman/cmderrCreate a new command error or wrap an existing one:
import "github.com/madman/cmderr"
// Simple error with a code
err := cmderr.New("INSUFFICIENT_FUNDS", "Account balance is too low")
// Wrap an existing error
wrapped := cmderr.Wrap("DB_ERROR", queryErr, "failed to update user")Use cmderr.From() at command boundaries to ensure every error is a *CommandError. It automatically maps common errors like context.DeadlineExceeded to predefined codes.
func (w *Worker) Process(ctx context.Context) error {
err := w.service.DoWork(ctx)
return cmderr.From(err) // Ensures result is serializable
}cmderr allows you to maintain type-safe error handling even after serialization.
Register how your domain error should be encoded to and decoded from a CommandError.
type LowBalanceError struct {
Amount float64
}
func (e *LowBalanceError) Error() string { return "balance too low" }
func init() {
cmderr.RegisterCodec[*LowBalanceError](
"BALANCE_LOW",
func(err *LowBalanceError) (string, map[string]any) {
return err.Error(), map[string]any{"amount": err.Amount}
},
func(ce *cmderr.CommandError) *LowBalanceError {
amount, _ := ce.Details["amount"].(float64)
return &LowBalanceError{Amount: amount}
},
)
}Now you can wrap your domain error and later reconstruct it using errors.As.
// In Service A:
err := &LowBalanceError{Amount: 10.50}
cmdErr := cmderr.WrapDomain("BALANCE_LOW", err)
// ... persist to DB or send over wire ...
// In Service B (after decoding):
var lowBalance *LowBalanceError
if errors.As(cmdErr, &lowBalance) {
fmt.Printf("Recovered amount: %.2f\n", lowBalance.Amount)
}cmderr provides helpers for JSON serialization that preserve the error chain and reconstruction capabilities.
// Encode for storage
bytes, err := cmdErr.EncodeJSON()
// Decode from storage
recoveredErr, err := cmderr.DecodeJSON(bytes)The package includes several common categories that you can use out of the box:
cmderr.ErrTimeout("TIMEOUT")cmderr.ErrCanceled("CANCELED")cmderr.ErrInternal("INTERNAL")
cmderr includes a built-in AI Agent Skill that provides procedural knowledge to AI coding assistants (like Claude Code, Cursor, etc.) on how to use this library correctly.
You can install the skill using the skills.sh CLI:
npx skills add madman/cmderrOr, if you are working directly in this repository, you can link the local skill:
./skills.sh link skills/cmderrOnce installed, your AI agent will:
- Know the best practices for error normalization and persistence.
- Automatically suggest using
cmderr.From(err)at command boundaries. - Help you register custom codecs and wrap domain errors correctly.
- Understand when to use
cmderrvs. standard Go errors.
Tip
Always use cmderr.From() when persisting command results to ensure a consistent, durable error schema in your database.