Best Practices for fabrica-util's Enhanced Error Handling with Context in go-pantheon Microservices

Use fabrica-util's errors.Wrapf and xsync.Go with automatic panic recovery to preserve full stack traces and context across asynchronous boundaries in go-pantheon microservices.

The go-pantheon/fabrica-util repository provides a production-ready errors package that extends github.com/pkg/errors with Go 1.20+ compatibility, delivering utilities for stack trace preservation, structured logging, and concurrent error aggregation. For distributed microservices architectures, these primitives ensure that failures carry complete diagnostic context from the point of origin through goroutine boundaries to centralized observability platforms.

Core Error Wrapping and Context Enrichment

The errors package in errors/errors.go provides drop-in replacements for standard error creation that capture execution context. Unlike fmt.Errorf, these utilities preserve the original error type and stack trace.

Creating and Wrapping Errors

Use errors.Wrap and errors.Wrapf when bubbling up errors from lower-level calls. These functions delegate to pkg/errors to maintain the original stack trace while adding domain-specific context.

// internal/dao/user.go
func (d *DAO) GetUser(id string) (*User, error) {
    u, err := d.db.QueryUser(id) // lower-level call returns a raw error
    if err != nil {
        // Preserve original error & stack, add high-level context
        return nil, errors.Wrapf(err, "failed to fetch user %s from DB", id)
    }
    return u, nil
}

Implementation note: Wrapf is defined at lines 31-34 in errors/errors.go as a thin wrapper around pkg/errors.Wrapf, ensuring stack trace continuity.

Attaching Messages Without Stack Alteration

When you need to add user-friendly notes without wrapping the underlying error, use errors.WithMessage or errors.WithMessagef. This preserves the original error type for errors.Is and errors.As checks while enriching the display message.

Preserving Stack Traces in Asynchronous Operations

Microservices rely heavily on background goroutines for tasks like cache warming, event processing, and timeouts. The xsync package in xsync/routines.go provides xsync.Go, which automatically recovers panics and logs structured error data including full stack traces.

Structured Goroutine Execution

xsync.Go accepts a function name, the executable function, and an optional error filter. It internally extracts stack traces using errors.StackTrace(err) and logs via slog at lines 70-81 in xsync/routines.go.

// service/worker.go
func (w *Worker) Start(ctx context.Context) {
    xsync.Go("process pending jobs", func() error {
        return w.processJobs()
    }, xsync.IsCancelled) // optional filter to silence cancel errors
}

The internal logging implementation produces structured output:

slog.Error("goroutine error occurred.",
    "message", msg,
    "error", err.Error(),
    "stack", errors.StackTrace(err))

Recovering from Panics as Typed Errors

Uncontrolled panics in goroutines can crash entire microservice instances. xsync.CatchErr transforms recovered panics into standard error values with complete stack traces, as implemented at lines 32-44 in xsync/routines.go.

Converting Panics to Wrapped Errors

When xsync.Go detects a panic, it invokes CatchErr to capture the panic value and wrap it:

func (s *Scheduler) runTask(task func()) {
    xsync.Go("run scheduled task", func() error {
        task() // may panic
        return nil
    })
}

The recovery mechanism produces an error via errors.Wrap(t, "goroutine panic recovered"), allowing upstream handlers to inspect the failure using standard Go error interfaces rather than managing raw panic values.

Aggregating Parallel Execution Errors

Concurrent microservice operations often generate multiple errors that require collation. The errors package provides SafeJoinError for thread-safe aggregation and JoinUnsimilar for deduplication.

Thread-Safe Error Collection

SafeJoinError (defined at line 27 in errors/errors.go) provides a concurrency-safe mechanism for collecting errors from multiple goroutines without data races.

func (p *Processor) ProcessAll(ctx context.Context, items []Item) error {
    var wg sync.WaitGroup
    errAgg := errors.NewSafeJoinError()

    for _, it := range items {
        wg.Add(1)
        it := it // capture loop variable
        xsync.Go("process item", func() error {
            defer wg.Done()
            if e := p.handle(it); e != nil {
                errAgg.Join(e) // thread-safe aggregation
            }
            return nil
        })
    }
    wg.Wait()
    if errAgg.HasError() {
        return errAgg // implements error, returns combined message
    }
    return nil
}

Deduplicating Similar Errors

For batch operations that may produce identical error messages, errors.JoinUnsimilar combines multiple errors while deduplicating similar messages, keeping telemetry payloads compact. This utility resides at lines 61-66 in errors/errors.go.

func validateBatch(batch []Record) error {
    var errs []error
    for _, r := range batch {
        if r.ID == "" {
            errs = append(errs, errors.New("record ID missing"))
        }
        // other checks...
    }
    return errors.JoinUnsimilar(errs...)
}

Summary

  • Always use errors.Wrapf to preserve stack traces when crossing function boundaries in go-pantheon microservices.
  • Leverage xsync.Go for automatic panic recovery and structured error logging with full stack traces.
  • Extract diagnostic data with errors.StackTrace for detailed observability in centralized logging systems.
  • Aggregate concurrent errors using SafeJoinError for thread safety or JoinUnsimilar to deduplicate similar failure messages from parallel workers.

Frequently Asked Questions

How does fabrica-util's errors.Wrap differ from fmt.Errorf?

errors.Wrap and errors.Wrapf preserve the original error's stack trace by delegating to github.com/pkg/errors, whereas fmt.Errorf with the %w verb only wraps the error value without capturing the call stack. This distinction is critical in microservices where you need to trace failures across process boundaries back to the originating line in errors/errors.go or consumer code.

When should I use SafeJoinError versus errors.Join?

Use SafeJoinError when aggregating errors from concurrent goroutines, as it provides thread-safe methods (Join, HasError) protected by internal synchronization. Use errors.Join for single-threaded error collection where concurrency safety is not required, such as sequential validation checks in a single request handler.

Can I filter which errors get logged in xsync.Go?

Yes. xsync.Go accepts an optional filter function parameter (such as xsync.IsCancelled) that receives the error before logging. If the filter returns true, the error is suppressed from logs. This prevents noise from expected cancellation errors during graceful shutdowns while still capturing genuine failures with full stack traces.

Where are the error handling utilities defined in the source code?

Core error utilities reside in errors/errors.go, including Wrapf, JoinUnsimilar, and SafeJoinError. Goroutine management and panic recovery are implemented in xsync/routines.go, which defines Go, Timeout, and CatchErr. Custom error definitions for lifecycle control appear in xsync/stopper.go, while xsync/future.go demonstrates asynchronous error propagation patterns.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →