cli/pkg/errors/errors.go

152 lines
3.6 KiB
Go
Raw Normal View History

// Package errors provides structured error handling for Core applications.
//
// Errors include operational context (what was being done) and support
// error wrapping for debugging while keeping user-facing messages clean:
//
// err := errors.E("user.Create", "email already exists", nil)
// err := errors.Wrap(dbErr, "user.Create", "failed to save user")
//
// // Check error types
// if errors.Is(err, sql.ErrNoRows) { ... }
//
// // Extract operation
// var e *errors.Error
// if errors.As(err, &e) {
// fmt.Println("Operation:", e.Op)
// }
package errors
import (
stderrors "errors"
"fmt"
)
// Error represents a structured error with operational context.
type Error struct {
Op string // Operation being performed (e.g., "user.Create")
Msg string // Human-readable message
Err error // Underlying error (optional)
Code string // Error code for i18n/categorisation (optional)
}
// E creates a new Error with operation context.
//
// err := errors.E("config.Load", "file not found", os.ErrNotExist)
// err := errors.E("api.Call", "rate limited", nil)
func E(op, msg string, err error) error {
return &Error{Op: op, Msg: msg, Err: err}
}
// Wrap wraps an error with operation context.
// Returns nil if err is nil.
//
// return errors.Wrap(err, "db.Query", "failed to fetch user")
func Wrap(err error, op, msg string) error {
if err == nil {
return nil
}
return &Error{Op: op, Msg: msg, Err: err}
}
// WrapCode wraps an error with operation context and an error code.
//
// return errors.WrapCode(err, "ERR_NOT_FOUND", "user.Get", "user not found")
func WrapCode(err error, code, op, msg string) error {
if err == nil && code == "" {
return nil
}
return &Error{Op: op, Msg: msg, Err: err, Code: code}
}
// Code creates an error with just a code and message.
//
// return errors.Code("ERR_VALIDATION", "invalid email format")
func Code(code, msg string) error {
return &Error{Code: code, Msg: msg}
}
// Error returns the error message.
func (e *Error) Error() string {
if e.Op != "" && e.Err != nil {
return fmt.Sprintf("%s: %s: %v", e.Op, e.Msg, e.Err)
}
if e.Op != "" {
return fmt.Sprintf("%s: %s", e.Op, e.Msg)
}
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Msg, e.Err)
}
return e.Msg
}
// Unwrap returns the underlying error.
func (e *Error) Unwrap() error {
return e.Err
}
// --- Standard library wrappers ---
// Is reports whether any error in err's tree matches target.
func Is(err, target error) bool {
return stderrors.Is(err, target)
}
// As finds the first error in err's tree that matches target.
func As(err error, target any) bool {
return stderrors.As(err, target)
}
// New returns an error with the given text.
func New(text string) error {
return stderrors.New(text)
}
// Join returns an error that wraps the given errors.
func Join(errs ...error) error {
return stderrors.Join(errs...)
}
// --- Helper functions ---
// Op extracts the operation from an error, or empty string if not an Error.
func Op(err error) string {
var e *Error
if As(err, &e) {
return e.Op
}
return ""
}
// ErrCode extracts the error code, or empty string if not set.
func ErrCode(err error) string {
var e *Error
if As(err, &e) {
return e.Code
}
return ""
}
// Message extracts the message from an error.
// For Error types, returns Msg; otherwise returns err.Error().
func Message(err error) string {
if err == nil {
return ""
}
var e *Error
if As(err, &e) {
return e.Msg
}
return err.Error()
}
// Root returns the deepest error in the chain.
func Root(err error) error {
for {
unwrapped := stderrors.Unwrap(err)
if unwrapped == nil {
return err
}
err = unwrapped
}
}