clierror

package
v0.0.0-...-fedf015 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

CLI Error Handling Package

pkg/clierror provides standard error handling infrastructure for CLI entry points in the commitgraph project.

Purpose

This package addresses a common issue across CLI tools: ensuring that:

  1. Panics are caught and logged with stack traces before exit
  2. Errors are logged consistently and exit codes are appropriate
  3. Main functions never return normally (always call os.Exit())
  4. Errors are wrapped with structured context for better debugging
  5. Exit codes follow standard conventions based on error categories

Usage

Basic pattern for all CLI entry points:

package main

import (
    "github.com/jedarden/commitgraph/pkg/clierror"
)

func main() {
    clierror.Run(run)
}

func run() error {
    // Your CLI logic here
    // Return nil on success
    // Return error on failure
    return nil
}

Features

Panic Recovery

If your run() function panics, clierror.Run will:

  1. Recover from the panic
  2. Log the panic value
  3. Log the full stack trace
  4. Exit with code 70 (EX_SOFTWARE), consistently across Run, RunSafe, RunWithExitCode, and RunSafeWithDefault
Error Handling

If your run() function returns an error, clierror.Run will:

  1. Print a concise message with the exit code, failed operation, and suggested action
  2. Log one structured JSON error record
  3. Exit with the appropriate code (determined by error category)
Structured error logs

All four process runners (Run, RunSafe, RunWithExitCode, and RunSafeWithDefault) print the same human-facing message to stderr before using the caught-error schema below. Structured records are JSON Lines with these stable top-level fields:

  • timestamp: UTC RFC 3339 timestamp
  • level: ERROR for a caught error or recovered panic
  • msg: caught CLI error or recovered CLI panic
  • entry_point: executable basename
  • catch_site: runner that caught the failure
  • error_type: classified error category or concrete Go error type
  • message: original error or panic message
  • exit_code: process exit code selected by the runner
  • formatted_error: canonical operational error format
  • stack_trace: present when carried by the error and always present for a recovered panic

Structured logging defaults to stderr at ERROR level, after the human-facing message. Configure it without changing a command's flags:

# Append caught errors to a mode-0600 JSONL file.
COMMITGRAPH_CLI_ERROR_LOG_OUTPUT=file \
COMMITGRAPH_CLI_ERROR_LOG_FILE=/var/log/commitgraph/cli-errors.jsonl \
command-name

# Supported minimum levels: DEBUG, INFO, WARN, ERROR, OFF.
COMMITGRAPH_CLI_ERROR_LOG_LEVEL=OFF command-name

COMMITGRAPH_CLI_ERROR_LOG_OUTPUT accepts stderr (default) or file. If a file cannot be opened or configuration is invalid, the original failure is still logged to stderr with a logging_configuration_error field. Successful commands do not initialize the error logger or create a configured file.

Error Categories and Exit Codes

The package provides standard error categories with default exit codes following common CLI conventions:

Category Exit Code Usage
CategoryUsage 2 Command-line argument/flag errors
CategoryInput 3 File validation, data parsing errors
CategoryNetwork 4 Connection failures, timeouts
CategoryPermission 5 Authorization/access control errors
CategoryInternal 70 Bugs, unexpected errors (should not occur)
CategoryTransient 75 Temporary failures (might succeed on retry)

Error Wrapping Patterns

Basic Wrapping with Context
func run() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return clierror.NewInput(
            "failed to read configuration file",
            err,
        )
    }
    // ... process data
    return nil
}
Wrapping with Additional Context
func run() error {
    data, err := os.ReadFile(filepath)
    if err != nil {
        return clierror.NewInputWithContext(
            "failed to read file",
            filepath,
            err,
        )
    }
    // ... process data
    return nil
}
Convenience Wrappers

The package provides convenience functions that only wrap if the error is non-nil:

func run() error {
    data, err := os.ReadFile("input.json")
    if err != nil {
        return clierror.WrapInput("failed to read input file", err)
    }

    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return clierror.WrapInput("failed to parse input JSON", err)
    }

    // ... process config
    return nil
}
Network Error Handling
func run() error {
    resp, err := http.Get(url)
    if err != nil {
        return clierror.WrapNetwork("failed to fetch data", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        return clierror.NewNetwork(
            fmt.Sprintf("server returned error status: %d", resp.StatusCode),
            fmt.Errorf("HTTP %d", resp.StatusCode),
        )
    }
    // ... process response
    return nil
}
Usage Error Handling
func run() error {
    if *inputPath == "" {
        return clierror.NewUsage(
            "missing required --input flag",
            fmt.Errorf("no input path specified"),
        )
    }
    // ... process
    return nil
}
All Conditional Wrappers

The conditional wrappers are useful when you want to add context but only if an error actually occurred:

func run() error {
    // Usage errors
    if err := validateFlags(); err != nil {
        return clierror.WrapUsage("flag validation failed", err)
    }

    // Input errors
    if err := parseInput(data); err != nil {
        return clierror.WrapInput("input parsing failed", err)
    }

    // Network errors
    if err := fetchRemoteData(); err != nil {
        return clierror.WrapNetwork("remote fetch failed", err)
    }

    // Permission errors
    if err := checkAccess(); err != nil {
        return clierror.WrapPermission("access check failed", err)
    }

    // Internal errors (bugs)
    if err := internalInvariantCheck(); err != nil {
        return clierror.WrapInternal("invariant violation", err)
    }

    // Transient errors (retryable)
    if err := attemptOperation(); err != nil {
        return clierror.WrapTransient("temporary failure", err)
    }

    return nil
}
Custom Exit Codes

You can override the default exit code for any category:

func run() error {
    if !isValid() {
        return clierror.NewWrapWithExitCode(
            clierror.CategoryInput,
            "validation failed",
            10, // Custom exit code
            fmt.Errorf("invalid data format"),
        )
    }
    return nil
}

Error Types

WrappedError

The primary error type that provides structured context:

type WrappedError struct {
    Category         ErrorCategory  // Error classification
    Message          string         // Human-readable description
    Context          string         // Additional context
    Err              error          // Underlying error
    ExitCodeOverride int            // Custom exit code
}
ExitCodeError (Legacy)

Legacy error type for simple exit code wrapping:

type ExitCodeError struct {
    ExitCode int
    Err      error
}

Helper Functions

Category-Specific Constructors
  • NewUsage(message, err) - Usage/argument errors (exit code 2)
  • NewInput(message, err) - Input/data errors (exit code 3)
  • NewInputWithContext(message, context, err) - Input errors with context
  • NewNetwork(message, err) - Network errors (exit code 4)
  • NewPermission(message, err) - Permission errors (exit code 5)
  • NewInternal(message, err) - Internal errors (exit code 70)
  • NewTransient(message, err) - Transient errors (exit code 75)
Conditional Wrappers

These functions only wrap if the error is non-nil (return nil if err == nil):

  • WrapInput(message, err) - Conditional input error wrapping
  • WrapInputWithContext(message, context, err) - Conditional with context
  • WrapNetwork(message, err) - Conditional network error wrapping
  • WrapUsage(message, err) - Conditional usage error wrapping
  • WrapPermission(message, err) - Conditional permission error wrapping
  • WrapInternal(message, err) - Conditional internal error wrapping
  • WrapTransient(message, err) - Conditional transient error wrapping
General Wrappers
  • NewWrap(category, message, err) - Generic wrapped error
  • NewWrapWithContext(category, message, context, err) - With context
  • NewWrapWithExitCode(category, message, exitCode, err) - Custom exit code
Utility Functions
  • GetExitCode(err) - Extract the outermost classified exit code from an error chain, including through standard %w wrappers

Testing

The package runner tests execute the current Go test binary as a subprocess so the real os.Exit() behavior is verified without mocking. For testing CLI tools:

  1. Extract the main logic into a separate run() function
  2. Test run() directly without calling clierror.Run
  3. clierror.Run is only for the actual main function
// In your test file
func TestRun(t *testing.T) {
    // Test the run function directly
    err := run()
    if err != nil {
        t.Errorf("run() failed: %v", err)
    }
}

Migration Guide

Before (no error catching):
func main() {
    if err := doSomething(); err != nil {
        log.Fatalf("Error: %v", err)
    }
}
After (with error catching):
func main() {
    clierror.Run(run)
}

func run() error {
    if err := doSomething(); err != nil {
        return clierror.WrapInput("operation failed", err)
    }
    return nil
}
Before (basic error wrapping):
func run() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return fmt.Errorf("failed to read config: %w", err)
    }
    // ... process
    return nil
}
After (structured error wrapping):
func run() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return clierror.NewInputWithContext(
            "failed to read configuration",
            "config.json",
            err,
        )
    }
    // ... process
    return nil
}

Complete Example

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "os"

    "github.com/jedarden/commitgraph/pkg/clierror"
)

func main() {
    clierror.Run(run)
}

func run() error {
    inputPath := flag.String("input", "", "Path to input file")
    flag.Parse()

    // Usage error handling
    if *inputPath == "" {
        return clierror.NewUsage(
            "missing required flag",
            fmt.Errorf("--input is required"),
        )
    }

    // Input reading with context
    data, err := os.ReadFile(*inputPath)
    if err != nil {
        return clierror.NewInputWithContext(
            "failed to read input file",
            *inputPath,
            err,
        )
    }

    // Input parsing
    var config Config
    if err := json.Unmarshal(data, &config); err != nil {
        return clierror.WrapInput("failed to parse input JSON", err)
    }

    // Network operation (if needed)
    if err := processData(&config); err != nil {
        return clierror.WrapNetwork("failed to process data", err)
    }

    fmt.Println("Processing complete!")
    return nil
}

Comprehensive Examples

The file examples.go contains 15 comprehensive examples covering common CLI error handling patterns:

  1. Basic usage error handling - Flag and argument validation
  2. File reading with context - Including file paths in error messages
  3. JSON parsing - Structured data parsing errors
  4. Network operations - HTTP requests and connection failures
  5. Database connections - Multiple error types in one operation
  6. Directory walking - Error accumulation during batch processing
  7. Conditional wrapping - Only wrapping when error occurs
  8. Custom exit codes - Overriding default exit codes
  9. Internal errors - Bug/invariant violations
  10. Transient errors - Retryable temporary failures
  11. Multi-step operations - Layered error handling
  12. Complete run() function - Full CLI structure
  13. Error accumulation - Collecting multiple errors
  14. Permission handling - Access control errors
  15. Panic-safe processing - Working with panic recovery

Use these examples as reference patterns when implementing or refactoring CLI commands.

# View the examples
cat pkg/clierror/examples.go

Quick Reference: Common Patterns

When to use each error category
Scenario Category Exit Code Function
Missing/invalid flags CategoryUsage 2 NewUsage, WrapUsage
File read/write errors CategoryInput 3 NewInput, WrapInput, NewInputWithContext
JSON parse errors CategoryInput 3 WrapInput
Connection failures CategoryNetwork 4 NewNetwork, WrapNetwork
Permission denied CategoryPermission 5 NewPermission, WrapPermission
Bugs/invariant violations CategoryInternal 70 NewInternal, WrapInternal
Temporary failures CategoryTransient 75 NewTransient, WrapTransient
Pattern: Include context when possible
// ❌ Bad: No context
return clierror.NewInput("file read failed", err)

// ✅ Good: Includes which file
return clierror.NewInputWithContext("file read failed", filepath, err)
Pattern: Use conditional wrappers
// These only wrap if err != nil (return nil if err is nil)
return clierror.WrapInput("failed to parse", err)
return clierror.WrapNetwork("connection failed", err)

See Also

  • pkg/clierror/examples.go - 15 comprehensive error wrapping examples
  • pkg/warmstart/error.go - Similar error handling patterns for warmstart parsing
  • Go's standard errors package for error wrapping
  • Unix/SysExit conventions for exit code meanings

Documentation

Overview

Package clierror provides reusable error catching templates and helpers.

This file contains defer/recover templates that wrap panics and errors in the standard error structure and map them to appropriate exit codes.

Usage Examples:

Simple panic recovery:

func myFunction() {
    defer clierror.RecoverPanic()
    // ... code that might panic
}

Panic recovery with custom handler:

func myFunction() {
    defer clierror.RecoverPanicWithHandler(func(r interface{}) error {
        return clierror.NewInternal("panic occurred", fmt.Errorf("%v", r))
    })
    // ... code that might panic
}

Comprehensive error catching in main():

func main() {
    clierror.RunSafe(run)
}

func run() error {
    // Your main logic here
    return nil
}

Package clierror provides utilities for CLI error handling, including commit SHA retrieval.

Package clierror provides standard error handling infrastructure for CLI entry points.

This package provides utilities for catching panics, wrapping errors with context, error categorization, and ensuring clean error propagation from CLI main functions without panics reaching the runtime.

Usage:

func main() {
    clierror.Run(run)
}

func run() error {
    // Your main logic here
    return nil
}

Package clierror provides standard error handling infrastructure for CLI entry points.

This file contains comprehensive examples of error wrapping patterns that can be used as reference when implementing or refactoring CLI commands.

Index

Examples

Constants

View Source
const (
	// ErrorLogOutputEnv selects the caught-error destination. Supported values
	// are "stderr" (the default) and "file".
	ErrorLogOutputEnv = "COMMITGRAPH_CLI_ERROR_LOG_OUTPUT"
	// ErrorLogFileEnv is the append-only destination used when
	// ErrorLogOutputEnv is set to "file".
	ErrorLogFileEnv = "COMMITGRAPH_CLI_ERROR_LOG_FILE"
	// ErrorLogLevelEnv sets the minimum slog level. Supported values are
	// DEBUG, INFO, WARN, ERROR, and OFF. The default is ERROR.
	ErrorLogLevelEnv = "COMMITGRAPH_CLI_ERROR_LOG_LEVEL"
)

Variables

DefaultExitCodes maps error categories to their default exit codes. These follow common CLI conventions: - 1: General errors - 2: Usage errors (like getopt) - 3: Input/data errors - 4: Network errors - 5: Permission errors - 70: Internal software errors (like sysexits.h) - 75: Transient/temporary errors

Functions

func Attach

func Attach(err error, key string, value interface{}) error

Attach adds structured context to an error without wrapping it in the error chain. This is useful for adding metadata that should be displayed with the error but shouldn't participate in error comparisons (errors.Is/As). If err is nil, returns nil.

func Attachf

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

Attachf adds formatted context to an error. This is the formatted version of Attach for dynamic attachment construction. If err is nil, returns nil.

func CatchPanic

func CatchPanic(fn func()) (err error)

CatchPanic is a generic panic catching template that returns an error if a panic occurred, or nil if the function completed successfully.

This is useful for inline panic catching without defer statements.

Example:

if err := clierror.CatchPanic(func() {
    // Code that might panic
    mightPanic()
}); err != nil {
    log.Printf("Caught panic: %v", err)
}

func CleanupAndRecover

func CleanupAndRecover(errPtr *error, cleanup func())

CleanupAndRecover combines cleanup execution with panic recovery. It ensures cleanup runs even if a panic occurs, then converts panic to error.

Example:

func process(data []byte) (err error) {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }

    defer clierror.CleanupAndRecover(&err, func() {
        file.Close()
    })

    // ... process file, potential panic here
    return nil
}

func DeferRecover

func DeferRecover(errPtr *error)

DeferRecover is a template function for deferred error recovery. It's useful when you need to perform cleanup and error handling together.

Example:

func process(data []byte) (err error) {
    defer clierror.DeferRecover(&err)
    file, err := os.Open("data.txt")
    if err != nil {
        return NewInputWithContext("failed to open file", "data.txt", err)
    }
    defer file.Close()
    // ... process file
    return nil
}

func FormatErrorMessage

func FormatErrorMessage(err error) string

FormatErrorMessage renders an error for a person running a CLI command. Unlike the repository's structured log formatter, this presentation keeps only the details needed to understand and correct the failure: the mapped exit code, the cause, the failed operation, validation field context, and a single suggested action.

func FormatErrorMessageAt

func FormatErrorMessageAt(operation string, err error) string

FormatErrorMessageAt is FormatErrorMessage with an explicit operation name. A non-empty operation overrides context carried by the error, which lets a command boundary describe ordinary errors that do not provide structured metadata themselves.

func FormatErrorMessageWithExitCode

func FormatErrorMessageWithExitCode(err error, exitCode int) string

FormatErrorMessageWithExitCode renders an error using the exit code the command boundary will actually return. Most callers should use FormatErrorMessage; runners use this variant when they intentionally override the fallback code for an otherwise unclassified error.

func GetCommitSHA

func GetCommitSHA() string

GetCommitSHA retrieves the commit SHA from the following sources in order: 1. The .needle-predispatch-sha file in the repository root 2. git rev-parse HEAD command (fallback) 3. Empty string if neither method succeeds

This function is called by entry points to obtain the commit SHA that should be threaded through all intermediate function calls to error constructors.

Returns the commit SHA as a string (40-character hexadecimal) or empty string if unavailable.

func GetExitCode

func GetExitCode(err error) int

GetExitCode extracts the appropriate exit code from an error chain. The outermost WrappedError or ExitCodeError wins, including when standard wrappers such as fmt.Errorf with %w appear outside the classified error. Typed service-layer errors from pkg/errors are translated by their category. Errors without either an exit-aware value or a recognized service category map to the general error code 1.

func Must

func Must(fn func() error)

Must executes a function and panics with an internal error if it returns an error. This is useful for initialization code that must succeed.

Example:

func init() {
    clierror.Must(loadConfig())
    clierror.Must(connectDatabase())
}

func loadConfig() error {
    // Load configuration
    return nil
}
Example
// Initialization that must succeed
initialize := func() error {
	return nil
}

// Use in init():
// func init() {
//     clierror.Must(initialize())
// }
_ = initialize

func MustValue

func MustValue[T any](fn func() (T, error)) T

MustValue executes a function that returns a value and an error. It panics with an internal error if the function returns an error, otherwise returns the value.

Example:

func main() {
    config := clierror.MustValue(loadConfig)
    _ = config
}

func loadConfig() (*Config, error) {
    cfg, err := readConfig()
    if err != nil {
        return nil, err
    }
    return cfg, nil
}

func ParseFlags

func ParseFlags(arguments []string) error

ParseFlags parses the process-wide flag set without allowing package flag to terminate the process itself. Invalid flags become usage errors so the shared runner can format the message, map exit code 2, and retain structured diagnostics. Help is returned as flag.ErrHelp and treated as success by the runners after flag has printed the command's usage text.

func RecoverAndReturn

func RecoverAndReturn(errPtr *error)

RecoverAndReturn is a defer/recover template for functions that return errors. It catches panics, converts them to errors, and assigns them to a pointer.

This pattern is useful when you want a function to return errors instead of panicking, making error handling more explicit.

Example:

func process(data []byte) (err error) {
    defer clierror.RecoverAndReturn(&err)
    var result interface{}
    if err := json.Unmarshal(data, &result); err != nil {
        return WrapInput("JSON parse failed", err)
    }
    _ = result
    return nil
}
Example
// Converting panics to returned errors
parseJSON := func(data []byte) (err error) {
	defer RecoverAndReturn(&err)
	var result map[string]interface{}
	if err := json.Unmarshal(data, &result); err != nil {
		panic(fmt.Sprintf("JSON parse failed: %v", err))
	}
	return nil
}

_ = parseJSON([]byte(`{"key": "value"}`))

func RecoverPanic

func RecoverPanic()

RecoverPanic is a simple defer/recover template that catches panics, logs them with stack trace, and converts them to internal errors.

This function should be used as a defer statement at the beginning of functions that might panic. It never returns normally - it panics again with a WrappedError containing the original panic value.

Example:

func process(data []byte) {
    defer clierror.RecoverPanic()
    var result interface{}
    if err := json.Unmarshal(data, &result); err != nil {
        panic(fmt.Sprintf("JSON parse failed: %v", err))
    }
    _ = result
}
Example

Example usage tests (documentation through tests)

// Simple panic recovery in a function
processData := func(data []byte) {
	defer RecoverPanic()
	// Code that might panic
	if len(data) == 0 {
		panic("no data to process")
	}
}

processData([]byte{1, 2, 3})

func RecoverPanicWithHandler

func RecoverPanicWithHandler(handler func(r interface{}) error)

RecoverPanicWithHandler is a defer/recover template that catches panics and passes them to a custom handler function for conversion to errors.

The handler function receives the panic value and should return an error (typically a WrappedError). If the handler returns nil, no panic is re-raised. Otherwise, the returned error is used as the panic value.

Example:

func process(data []byte) {
    defer clierror.RecoverPanicWithHandler(func(r interface{}) error {
        errMsg := fmt.Sprintf("processing failed: %v", r)
        return clierror.NewInput("data processing error", fmt.Errorf(errMsg))
    })
    // ... code that might panic
}

func Run

func Run(fn func(commitSHA string) error)

Run executes a function, catching panics and handling errors cleanly.

If fn returns an error, Run prints it to stderr and exits with the appropriate code (determined by the error type: 1 for general errors, or custom codes from ExitCodeError/WrappedError).

If fn panics, Run recovers, logs the panic with stack trace, and exits with code 70 (EX_SOFTWARE), consistently with the other runner variants.

Run retrieves the commit SHA using GetCommitSHA() and passes it to fn.

Run never returns - it always calls os.Exit().

func RunSafe

func RunSafe(fn func() error)

RunSafe executes a function with comprehensive panic and error handling. It catches panics, handles errors, and exits with appropriate codes.

This is the main entry point template for CLI applications. It combines panic recovery with error handling and exit code mapping.

If fn panics, RunSafe logs the panic with stack trace and exits with code 70. If fn returns an error, RunSafe prints the error and exits with the appropriate code based on the error type. If fn succeeds, RunSafe exits with code 0.

Example:

func main() {
    clierror.RunSafe(run)
}

func run() error {
    // Your main logic here
    if *inputPath == "" {
        return NewUsage("missing required flag",
            errors.New("--input is required"))
    }
    return nil
}
Example
// Main entry point with panic and error handling
run := func() error {
	// Your main logic here
	return nil
}

// Use in main():
// func main() {
//     clierror.RunSafe(run)
// }
_ = run

func RunSafeWithDefault

func RunSafeWithDefault(fn func() error, defaultExitCode int)

RunSafeWithDefault executes a function with panic/error handling and a default exit code for unspecified errors.

This is similar to RunSafe but allows specifying a custom exit code for general errors (instead of the default 1).

Example:

func main() {
    clierror.RunSafeWithDefault(run, 2) // Use exit code 2 for general errors
}

func RunWithExitCode

func RunWithExitCode(fn func(commitSHA string) error, successCode int)

RunWithExitCode executes a function, catching panics and handling errors cleanly.

If fn returns an error, RunWithExitCode prints it to stderr and exits with the appropriate code (determined by the error type: 1 for general errors, or custom codes from ExitCodeError/WrappedError).

If fn panics, RunWithExitCode recovers, logs the panic with stack trace, and exits with code 70 (EX_SOFTWARE), consistently with the other runner variants.

If fn returns successfully, RunWithExitCode exits with the provided success code.

RunWithExitCode retrieves the commit SHA using GetCommitSHA() and passes it to fn.

RunWithExitCode never returns - it always calls os.Exit().

func Safe

func Safe(fn func() error) (err error)

Safe executes a function and returns any error (or panic converted to error). This ensures a function never panics - all panics are converted to errors.

Example:

if err := clierror.Safe(func() error {
    // Code that might panic or return an error
    return process(data)
}); err != nil {
    log.Printf("Operation failed: %v", err)
}
Example
// Ensure a function never panics
result := Safe(func() error {
	// Code that might panic or return an error
	return nil
})

_ = result

func WithErrorRecovery

func WithErrorRecovery(fn func() error) func() error

WithErrorRecovery is a higher-order function that wraps a function with panic recovery. The wrapped function will convert any panic to an error.

This is useful for creating panic-safe function wrappers.

Example:

var safeProcess = clierror.WithErrorRecovery(process)

err := safeProcess(data)
if err != nil {
    log.Printf("Process failed: %v", err)
}

func WrapInput

func WrapInput(message string, err error) error

WrapInput is a convenience function that wraps an error with input category only if the error is non-nil. Returns nil if err is nil.

func WrapInputWithContext

func WrapInputWithContext(message, context string, err error) error

WrapInputWithContext is a convenience function that wraps an error with input category and context only if the error is non-nil.

func WrapInternal

func WrapInternal(message string, err error) error

WrapInternal is a convenience function that wraps an error with internal category only if the error is non-nil.

func WrapNetwork

func WrapNetwork(message string, err error) error

WrapNetwork is a convenience function that wraps an error with network category only if the error is non-nil.

func WrapNetworkWithContext

func WrapNetworkWithContext(message, context string, err error) error

WrapNetworkWithContext is a convenience function that wraps an error with network category and context only if the error is non-nil.

func WrapPermission

func WrapPermission(message string, err error) error

WrapPermission is a convenience function that wraps an error with permission category only if the error is non-nil.

func WrapTransient

func WrapTransient(message string, err error) error

WrapTransient is a convenience function that wraps an error with transient category only if the error is non-nil.

func WrapUsage

func WrapUsage(message string, err error) error

WrapUsage is a convenience function that wraps an error with usage category only if the error is non-nil.

func Wrapf

func Wrapf(category ErrorCategory, format string, args ...interface{}) error

Wrapf creates a new WrappedError with a formatted message using fmt.Sprintf syntax. This is the formatted version of NewWrap for dynamic message construction. Returns nil if err is nil.

func WrapfWithContext

func WrapfWithContext(category ErrorCategory, format, context string, args ...interface{}) error

WrapfWithContext creates a new WrappedError with a formatted message and context. Returns nil if err is nil.

Types

type ErrorCategory

type ErrorCategory string

ErrorCategory represents a classification of errors for consistent exit code mapping.

const (
	// CategoryUsage indicates command-line usage errors (invalid flags, missing arguments, etc.)
	CategoryUsage ErrorCategory = "usage"
	// CategoryInput indicates input validation or data errors (invalid files, malformed data, etc.)
	CategoryInput ErrorCategory = "input"
	// CategoryNetwork indicates network-related errors (connection failures, timeouts, etc.)
	CategoryNetwork ErrorCategory = "network"
	// CategoryPermission indicates permission or authorization errors
	CategoryPermission ErrorCategory = "permission"
	// CategoryInternal indicates internal errors that should not occur (bugs, panics, etc.)
	CategoryInternal ErrorCategory = "internal"
	// CategoryTransient indicates transient errors that might succeed on retry
	CategoryTransient ErrorCategory = "transient"
)

type ErrorLogConfig

type ErrorLogConfig struct {
	Output   io.Writer
	Fallback io.Writer
	Level    slog.Leveler
}

ErrorLogConfig configures the structured logger used at CLI error boundaries. Output defaults to stderr, Fallback defaults to stderr, and Level defaults to ERROR when omitted.

type ErrorLogger

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

ErrorLogger emits one JSON object per caught CLI error. Its stable top-level fields are timestamp, level, msg, entry_point, catch_site, error_type, message, exit_code, and formatted_error. stack_trace is added when an error already carries one or when a panic is recovered.

func NewErrorLogger

func NewErrorLogger(config ErrorLogConfig) *ErrorLogger

NewErrorLogger creates a JSON error logger with an explicitly configured writer and minimum level. It is useful for embedding, tests, and callers that manage a file destination themselves.

func (*ErrorLogger) LogCaughtError

func (l *ErrorLogger) LogCaughtError(entryPoint, catchSite string, err error, exitCode int) error

LogCaughtError logs an error returned to a CLI runner. Logging failures are returned after a best-effort structured fallback write; callers should keep propagating the original operation error.

func (*ErrorLogger) LogRecoveredPanic

func (l *ErrorLogger) LogRecoveredPanic(entryPoint, catchSite string, value any, exitCode int) error

LogRecoveredPanic logs a panic recovered by a CLI runner and includes the recovery stack captured at the catch site.

type ExitCodeError

type ExitCodeError struct {
	// ExitCode is the code to exit with (defaults to 1 if 0).
	ExitCode int

	// Err is the underlying error.
	Err error
}

ExitCodeError is an error that carries a specific exit code.

func NewExitCodeError

func NewExitCodeError(code int, err error) *ExitCodeError

NewExitCodeError creates a new ExitCodeError with the given code and underlying error.

func (*ExitCodeError) Error

func (e *ExitCodeError) Error() string

Error implements the error interface.

func (*ExitCodeError) ErrorLogFormat

func (e *ExitCodeError) ErrorLogFormat() commiterrors.LogFormat

ErrorLogFormat adapts ExitCodeError to the standard logging contract while preserving its native Error() output and error chain.

func (*ExitCodeError) Unwrap

func (e *ExitCodeError) Unwrap() error

Unwrap returns the underlying error for errors.Is/As compatibility.

type WrappedError

type WrappedError struct {
	// Category classifies the error type for exit code mapping
	Category ErrorCategory

	// Message is a human-readable description of what operation failed
	Message string

	// Context provides additional context about the error
	Context string

	// Err is the underlying error (may be nil for standalone errors)
	Err error

	// ExitCodeOverride allows overriding the default exit code for this category
	ExitCodeOverride int
}

WrappedError provides rich context wrapping for errors with categorization. It supports standard error wrapping patterns while adding structured context for better error messages and exit code mapping.

func NewInput

func NewInput(message string, err error) *WrappedError

NewInput creates a new input error (exit code 3 by default). Use this for file validation, data parsing, and input errors.

func NewInputWithContext

func NewInputWithContext(message, context string, err error) *WrappedError

NewInputWithContext creates a new input error with additional context. Use this when you need to specify which file or input source failed.

func NewInternal

func NewInternal(message string, err error) *WrappedError

NewInternal creates a new internal error (exit code 70 by default). Use this for bugs and errors that should never occur.

func NewNetwork

func NewNetwork(message string, err error) *WrappedError

NewNetwork creates a new network error (exit code 4 by default). Use this for connection failures, timeouts, and HTTP errors.

func NewNetworkWithContext

func NewNetworkWithContext(message, context string, err error) *WrappedError

NewNetworkWithContext creates a new network error with additional context. Use this when you need to specify which host or endpoint failed.

func NewPermission

func NewPermission(message string, err error) *WrappedError

NewPermission creates a new permission error (exit code 5 by default). Use this for authorization and access control errors.

func NewTransient

func NewTransient(message string, err error) *WrappedError

NewTransient creates a new transient error (exit code 75 by default). Use this for temporary failures that might succeed on retry.

func NewUsage

func NewUsage(message string, err error) *WrappedError

NewUsage creates a new usage error (exit code 2 by default). Use this for command-line argument and flag errors.

func NewWrap

func NewWrap(category ErrorCategory, message string, err error) *WrappedError

NewWrap creates a new WrappedError with the given category, message, and underlying error. This is the most common wrapping function for adding context to errors.

func NewWrapWithContext

func NewWrapWithContext(category ErrorCategory, message, context string, err error) *WrappedError

NewWrapWithContext creates a new WrappedError with additional context string. Use this when you need to provide extra context about what failed.

func NewWrapWithExitCode

func NewWrapWithExitCode(category ErrorCategory, message string, exitCode int, err error) *WrappedError

NewWrapWithExitCode creates a new WrappedError with a custom exit code. Use this when you need to override the default exit code for a category.

func (*WrappedError) Error

func (e *WrappedError) Error() string

Error implements the error interface, providing a formatted error message.

func (*WrappedError) ErrorLogFormat

func (e *WrappedError) ErrorLogFormat() commiterrors.LogFormat

ErrorLogFormat exposes WrappedError's category, cause, context, and recovery to the repository-wide formatter without changing its long-standing Error() string contract.

func (*WrappedError) ExitCode

func (e *WrappedError) ExitCode() int

ExitCode returns the appropriate exit code for this error. It uses ExitCodeOverride if set, otherwise looks up the default for the category.

func (*WrappedError) MarshalJSON

func (e *WrappedError) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler interface for WrappedError. It serializes the error message string instead of the error interface.

func (*WrappedError) UnmarshalJSON

func (e *WrappedError) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler interface for WrappedError. It reconstructs the error from the serialized message string.

func (*WrappedError) Unwrap

func (e *WrappedError) Unwrap() error

Unwrap returns the underlying error for errors.Is/As compatibility.

Jump to

Keyboard shortcuts

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