package xerrors import ( "encoding/json" "fmt" ) // Err is a structured application error carrying a [Code], a human-readable // message, an optional cause, and optional key-value context fields. // // It implements the standard error interface, [errors.Unwrap] for cause chaining, // and [json.Marshaler] for API responses. It also satisfies the private duck-typing // interfaces that logz uses internally to enrich log records — without either // package importing the other. // // Use the builder methods [Err.WithContext] and [Err.WithError] to attach // additional information after construction: // // err := xerrors.New(xerrors.ErrInvalidInput, "validation failed"). // WithContext("field", "email"). // WithContext("rule", "required"). // WithError(cause) type Err struct { code Code message string err error fields map[string]any } // New creates an Err with the given code and message. No cause is set. func New(code Code, message string) *Err { return &Err{ code: code, message: message, } } // Wrap creates an Err that wraps an existing error with a code and message. // The wrapped error is accessible via [errors.Is], [errors.As], and [Err.Unwrap]. func Wrap(code Code, message string, err error) *Err { return &Err{ code: code, message: message, err: err, } } // InvalidInput creates an Err with [ErrInvalidInput] code. // msg is formatted with args using [fmt.Sprintf] rules. func InvalidInput(msg string, args ...any) *Err { return New(ErrInvalidInput, fmt.Sprintf(msg, args...)) } // NotFound creates an Err with [ErrNotFound] code. // msg is formatted with args using [fmt.Sprintf] rules. func NotFound(msg string, args ...any) *Err { return New(ErrNotFound, fmt.Sprintf(msg, args...)) } // Internal creates an Err with [ErrInternal] code. // msg is formatted with args using [fmt.Sprintf] rules. func Internal(msg string, args ...any) *Err { return New(ErrInternal, fmt.Sprintf(msg, args...)) } // WithContext adds a key-value pair to the error's context fields and returns // the receiver for chaining. Calling it multiple times with the same key // overwrites the previous value. func (e *Err) WithContext(key string, value any) *Err { if e.fields == nil { e.fields = make(map[string]any) } e.fields[key] = value return e } // WithError sets the underlying cause and returns the receiver for chaining. func (e *Err) WithError(err error) *Err { e.err = err return e } // Error implements the error interface. // Format: "INVALID_ARGUMENT: username is required → original cause" func (e *Err) Error() string { base := fmt.Sprintf("%s: %s", e.code, e.message) if e.err != nil { base = fmt.Sprintf("%s → %v", base, e.err) } return base } // Unwrap returns the underlying cause, enabling [errors.Is] and [errors.As] // to walk the full cause chain. func (e *Err) Unwrap() error { return e.err } // Code returns the typed error code. func (e *Err) Code() Code { return e.code } // Message returns the human-readable error message. func (e *Err) Message() string { return e.message } // Fields returns a shallow copy of the context fields. // Returns an empty (non-nil) map if no fields have been set. func (e *Err) Fields() map[string]any { if len(e.fields) == 0 { return map[string]any{} } out := make(map[string]any, len(e.fields)) for k, v := range e.fields { out[k] = v } return out } // Detailed returns a verbose string useful for debugging. // Format: "code: X | message: Y | cause: Z | fields: {...}" func (e *Err) Detailed() string { s := fmt.Sprintf("code: %s | message: %s", e.code, e.message) if e.err != nil { s = fmt.Sprintf("%s | cause: %v", s, e.err) } if len(e.fields) > 0 { s = fmt.Sprintf("%s | fields: %v", s, e.fields) } return s } // ErrorCode returns the string value of the error code. // // This method satisfies the private errorWithCode interface that logz defines // internally. Passing an *Err to logger.Error automatically enriches the log // record with an error_code field — without xerrors importing logz. func (e *Err) ErrorCode() string { return string(e.code) } // ErrorContext returns the raw context fields map. // // This method satisfies the private errorWithContext interface that logz defines // internally. The returned map is used read-only by logz; callers who need a // safe copy should use [Err.Fields] instead. func (e *Err) ErrorContext() map[string]any { return e.fields } // MarshalJSON implements [json.Marshaler]. // Output: {"code":"NOT_FOUND","message":"user not found","fields":{"id":"42"}} func (e *Err) MarshalJSON() ([]byte, error) { return json.Marshal(struct { Code string `json:"code"` Message string `json:"message"` Fields map[string]any `json:"fields,omitempty"` }{ Code: string(e.code), Message: e.message, Fields: e.fields, }) }