errors

package
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 2 Imported by: 0

README

Error Handling Package

This package provides a comprehensive error handling system for UniRTM, implementing custom error types, error classification, and error wrapping patterns as specified in Requirements 12.1 and 12.2.

Features

  • Sentinel Errors: Pre-defined error constants for common error conditions
  • Error Classification: Categorize errors into user, system, and external errors
  • Error Wrapping: Context-preserving error wrapping using fmt.Errorf with %w
  • Error Chain Support: Full support for errors.Is() and errors.As()
  • Exit Code Mapping: Automatic exit code determination based on error category

Sentinel Errors

The package defines six sentinel errors that can be used with errors.Is():

var (
    ErrNotFound          = errors.New("not found")
    ErrAlreadyExists     = errors.New("already exists")
    ErrInvalidConfig     = errors.New("invalid configuration")
    ErrNetworkFailure    = errors.New("network failure")
    ErrChecksumMismatch  = errors.New("checksum mismatch")
    ErrTransactionFailed = errors.New("transaction failed")
)
Usage Example
user, err := repo.FindByID(ctx, id)
if err != nil {
    if errors.Is(err, errors.ErrNotFound) {
        return nil, echo.ErrNotFound
    }
    return nil, err
}

Error Classification

Errors are classified into three categories, each with specific handling requirements:

1. User Errors (Exit Code: 1)

Invalid input, configuration errors, version not found.

Characteristics:

  • Return descriptive error messages
  • Suggest corrective actions
  • Safe to display to end users

Example:

if version == "" {
    return errors.NewUserError("version not specified", errors.ErrInvalidConfig)
}
2. System Errors (Exit Code: 2)

Disk full, permission denied, database corruption.

Characteristics:

  • Log full error context
  • Return generic user-safe messages
  • Require system-level intervention

Example:

if err := os.WriteFile(path, data, 0644); err != nil {
    return errors.NewSystemError("failed to write file", err)
}
3. External Errors (Exit Code: 3)

Network failures, backend API errors, download failures.

Characteristics:

  • Implement retry logic
  • Return wrapped errors with context
  • May be transient

Example:

resp, err := http.Get(url)
if err != nil {
    return errors.NewExternalError("failed to fetch release", errors.ErrNetworkFailure)
}

Error Wrapping

Use the Wrap() function to add context to errors while preserving the error chain:

user, err := repo.FindByID(ctx, id)
if err != nil {
    return nil, errors.Wrap(err, "find user %d", id)
}

This produces error messages like: find user 123: not found

Multiple Wrapping Layers

Errors can be wrapped multiple times to build a context chain:

// Layer 1: Repository
if err := db.Query(...); err != nil {
    return errors.Wrap(err, "query users table")
}

// Layer 2: Service
users, err := repo.FindAll(ctx)
if err != nil {
    return errors.Wrap(err, "fetch all users")
}

// Layer 3: Handler
users, err := service.GetUsers(ctx)
if err != nil {
    return errors.Wrap(err, "handle GET /users")
}

Result: handle GET /users: fetch all users: query users table: connection refused

Error Checking

Check Error Category
if errors.IsUserError(err) {
    // Display error message to user
    fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    os.Exit(1)
}

if errors.IsSystemError(err) {
    // Log full context, show generic message to user
    logger.Error("system error", "error", err)
    fmt.Fprintf(os.Stderr, "A system error occurred. Please contact support.\n")
    os.Exit(2)
}

if errors.IsExternalError(err) {
    // Retry or show transient error message
    fmt.Fprintf(os.Stderr, "External service error: %v\n", err)
    os.Exit(3)
}
Get Exit Code
if err != nil {
    os.Exit(errors.ExitCode(err))
}
Check Specific Errors
if errors.Is(err, errors.ErrNotFound) {
    // Handle not found case
}

if errors.Is(err, errors.ErrChecksumMismatch) {
    // Handle checksum mismatch
}

Best Practices

1. Always Wrap Errors with Context

Bad:

if err != nil {
    return err
}

Good:

if err != nil {
    return errors.Wrap(err, "install tool %s version %s", tool, version)
}
2. Use Appropriate Error Categories

Bad:

return errors.New("invalid version")

Good:

return errors.NewUserError("invalid version format", errors.ErrInvalidConfig)
3. Preserve Error Chains

Bad:

if err != nil {
    return fmt.Errorf("operation failed: %s", err.Error())
}

Good:

if err != nil {
    return errors.Wrap(err, "operation failed")
}
4. Check Errors with errors.Is()

Bad:

if err.Error() == "not found" {
    // ...
}

Good:

if errors.Is(err, errors.ErrNotFound) {
    // ...
}
5. Use Typed Errors for Structured Data

When you need to attach structured data to an error:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s — %s", e.Field, e.Message)
}

// Usage
if email == "" {
    return errors.NewUserError("validation failed", &ValidationError{
        Field:   "email",
        Message: "email is required",
    })
}

Integration with Logging

Errors should be logged with full context before being returned to users:

if err != nil {
    logger.Error("operation failed",
        "error", err,
        "tool", tool,
        "version", version,
        "category", errors.GetCategory(err).String(),
    )
    return errors.Wrap(err, "install tool %s@%s", tool, version)
}

Testing

The package includes comprehensive unit tests covering:

  • Sentinel error definitions
  • Error category classification
  • Error wrapping and unwrapping
  • Error chain preservation
  • Exit code mapping
  • Multiple wrapping layers

Run tests with:

go test ./internal/pkg/errors/...

References

Documentation

Overview

Example (ErrorHandlingPattern)

Example_errorHandlingPattern demonstrates a complete error handling pattern.

package main

import (
	"fmt"
	"os"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	// Simulate a function that might fail
	findUser := func(id int) error {
		// Simulate not found
		return errors.ErrNotFound
	}

	// Call the function
	err := findUser(123)
	if err != nil {
		// Wrap with context
		err = errors.Wrap(err, "find user %d", 123)

		// Categorize as user error
		err = errors.NewUserError("user lookup failed", err)

		// Check category and handle appropriately
		if errors.IsUserError(err) {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			// In real code: os.Exit(errors.ExitCode(err))
		}
	}
}
Example (MultipleWrapping)

Example_multipleWrapping demonstrates wrapping errors multiple times.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	// Start with a base error
	baseErr := errors.ErrNetworkFailure

	// Wrap at different layers
	layer1 := errors.Wrap(baseErr, "download artifact")
	layer2 := errors.Wrap(layer1, "install tool node")
	layer3 := errors.NewExternalError("installation failed", layer2)

	fmt.Println(layer3)
}
Output:
[external] installation failed: install tool node: download artifact: network failure

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound indicates a resource was not found.
	ErrNotFound = errors.New("not found")

	// ErrAlreadyExists indicates a resource already exists.
	ErrAlreadyExists = errors.New("already exists")

	// ErrInvalidConfig indicates invalid configuration.
	ErrInvalidConfig = errors.New("invalid configuration")

	// ErrNetworkFailure indicates a network operation failed.
	ErrNetworkFailure = errors.New("network failure")

	// ErrChecksumMismatch indicates checksum verification failed.
	ErrChecksumMismatch = errors.New("checksum mismatch")

	// ErrTransactionFailed indicates a transaction failed.
	ErrTransactionFailed = errors.New("transaction failed")
)

Sentinel errors for common error conditions. These errors can be used with errors.Is() for error checking.

Functions

func ExitCode

func ExitCode(err error) int

ExitCode returns the appropriate exit code for an error based on its category. User errors: 1, System errors: 2, External errors: 3, Unknown: 1

Example

ExampleExitCode demonstrates getting the exit code for an error.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	userErr := errors.NewUserError("test", nil)
	systemErr := errors.NewSystemError("test", nil)
	externalErr := errors.NewExternalError("test", nil)

	fmt.Println("User error exit code:", errors.ExitCode(userErr))
	fmt.Println("System error exit code:", errors.ExitCode(systemErr))
	fmt.Println("External error exit code:", errors.ExitCode(externalErr))
}
Output:
User error exit code: 1
System error exit code: 2
External error exit code: 3

func IsExternalError

func IsExternalError(err error) bool

IsExternalError checks if an error is an external error.

func IsSystemError

func IsSystemError(err error) bool

IsSystemError checks if an error is a system error.

func IsUserError

func IsUserError(err error) bool

IsUserError checks if an error is a user error.

Example

ExampleIsUserError demonstrates checking if an error is a user error.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	err := errors.NewUserError("test", nil)
	fmt.Println(errors.IsUserError(err))
}
Output:
true

func NewExternalError

func NewExternalError(message string, err error) error

NewExternalError creates a new external error with the given message and optional wrapped error. External errors indicate network failures, backend API errors, or download failures.

Example

ExampleNewExternalError demonstrates creating an external error.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	err := errors.NewExternalError("API request failed", errors.ErrNetworkFailure)
	fmt.Println(err)
}
Output:
[external] API request failed: network failure

func NewSystemError

func NewSystemError(message string, err error) error

NewSystemError creates a new system error with the given message and optional wrapped error. System errors indicate disk full, permission denied, or database corruption.

Example

ExampleNewSystemError demonstrates creating a system error.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	err := errors.NewSystemError("database connection failed", errors.ErrTransactionFailed)
	fmt.Println(err)
}
Output:
[system] database connection failed: transaction failed

func NewUserError

func NewUserError(message string, err error) error

NewUserError creates a new user error with the given message and optional wrapped error. User errors indicate invalid input, configuration errors, or version not found.

Example

ExampleNewUserError demonstrates creating a user error.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	err := errors.NewUserError("invalid version format", errors.ErrInvalidConfig)
	fmt.Println(err)
}
Output:
[user] invalid version format: invalid configuration

func Wrap

func Wrap(err error, format string, args ...interface{}) error

Wrap wraps an error with additional context using fmt.Errorf with %w. This preserves the error chain for errors.Is() and errors.As().

Example:

user, err := repo.FindByID(ctx, id)
if err != nil {
    return nil, Wrap(err, "find user %d", id)
}
Example

ExampleWrap demonstrates wrapping an error with context.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	baseErr := errors.ErrNotFound
	wrappedErr := errors.Wrap(baseErr, "find user %d", 123)
	fmt.Println(wrappedErr)
}
Output:
find user 123: not found

Types

type CategorizedError

type CategorizedError struct {
	// contains filtered or unexported fields
}

CategorizedError wraps an error with a category classification.

func (*CategorizedError) Category

func (e *CategorizedError) Category() ErrorCategory

Category returns the error category.

func (*CategorizedError) Context

func (e *CategorizedError) Context() string

Context returns the error context.

func (*CategorizedError) Error

func (e *CategorizedError) Error() string

Error implements the error interface.

func (*CategorizedError) Unwrap

func (e *CategorizedError) Unwrap() error

Unwrap returns the wrapped error for errors.Is() and errors.As() support.

type ErrorCategory

type ErrorCategory int

ErrorCategory represents the classification of an error.

const (
	// CategoryUnknown represents an unclassified error.
	CategoryUnknown ErrorCategory = iota

	// CategoryUser represents user errors (invalid input, configuration errors, version not found).
	// These errors should return descriptive messages and suggest corrective actions.
	// Exit code: 1
	CategoryUser

	// CategorySystem represents system errors (disk full, permission denied, database corruption).
	// These errors should log full context and return generic user-safe messages.
	// Exit code: 2
	CategorySystem

	// CategoryExternal represents external errors (network failures, backend API errors, download failures).
	// These errors should implement retry logic and return wrapped errors with context.
	// Exit code: 3
	CategoryExternal
)

func GetCategory

func GetCategory(err error) ErrorCategory

GetCategory returns the category of an error. If the error is not a CategorizedError, it returns CategoryUnknown.

Example

ExampleGetCategory demonstrates getting the category of an error.

package main

import (
	"fmt"

	"github.com/snowdreamtech/unirtm/internal/pkg/errors"
)

func main() {
	err := errors.NewUserError("test", nil)
	category := errors.GetCategory(err)
	fmt.Println(category)
}
Output:
user

func (ErrorCategory) String

func (c ErrorCategory) String() string

String returns the string representation of the error category.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL