oops

repository·main·Indexed 21 days ago

https://github.com/samber/oops

A structured error handling library for Go that provides rich contextual information, stack traces, and source code fragments. Designed as a drop-in replacement for the standard error interface, it complements existing logging toolchains like zap, zerolog, logrus, and slog by providing structured data including error codes, domain-specific tags, and execution context.

Tokens
24.8K
Snippets
85
Records
108
Agent score
76%

What's inside oops

  1. Overview of Oops structured error handling

    main

    Oops is a structured error management library for Go designed to provide rich contextual information during error handling. It acts as a drop-in replacement for the standard error interface.

    Key capabilities include:

    • Rich Context: Adding structured attributes and user metadata.
    • Debugging: Automatic stack traces and source code fragments.
    • Error Chaining: Wrapping and composing errors.
    • Panic Recovery: Built-in handling and conversion.
    • Assertions: One-line helpers for validation.

    Note: Oops is NOT a logging library. It is designed to complement your existing logging toolchain (e.g., zap, zerolog, logrus, slog, go-sentry) by providing the structured data that those loggers can then emit.

  2. Understand the structured error output of Oops

    main

    Oops provides rich, structured error data that can be exported in multiple formats. This data includes error codes, domain-specific tags, execution context (like user IDs), hints for troubleshooting (e.g., runbook links), and detailed stacktraces.

    When using Oops with loggers, you can expect outputs in formats like plain text (for human readability) or JSON (for machine processing and observability platforms).

    {
      "code": "iam_missing_permission",
      "context": {
        "user_id": 1234
      },
      "domain": "authz",
      "tags": [
        "iam",
        "authz"
      ],
      "error": "Permission denied",
      "hint": "Runbook: https://doc.acme.org/doc/abcd.md",
      "time": "2023-05-02T05:26:48.570837Z",
      "duration": "42ms",
      "stacktrace": "Oops: permission denied\n  --- at github.com/samber/oops/loggers/slog/example.go:20 (d)",
      "trace": "4ab0e35e-8414-4d76-b09e-cba80c983e4b",
      "user": {
        "firstname": "john",
        "id": "user-123",
        "lastname": "doe"
      }
    }
  3. Use the Error Builder to create errors

    main

    The oops library uses a builder pattern to create context-rich errors. You can chain methods to add metadata like domains, tags, and user information.

    Important: An oops.OopsErrorBuilder chain must always terminate with one of the following 'terminal' methods to return an error object:

    • .New(message string)
    • .Errorf(format string, args ...any)
    • .Wrap(err error)
    • .Wrapf(err error, format string, args ...any)
    • .Join(err1 error, err2 error, ...)
    • .Recover(cb func())
    • .Recoverf(cb func(), format string, args ...any)
    // Example of a complete builder chain
    err := oops.
        In("repository").
        Tags("database", "sql").
        Errorf("could not fetch user")
  4. Use Wrap and Wrapf correctly

    main

    Chaining Behavior

    Warning: oops.Wrapf(err, "msg: %w", otherErr) does not chain otherErr using the standard Go %w verb. The %w verb is treated only as a string formatting instruction. The resulting error chain is always result → err. If you require standard Go error chaining (where the new error wraps the old one), use oops.Errorf("msg: %w", otherErr) instead.

    Nil Safety

    oops.Wrap(...) and oops.Wrapf(...) return nil if the provided error is nil. To avoid redundant if err != nil checks, pass the function call directly into the wrapper.

    // ❌ Avoid this redundant check:
    err := mayFail()
    if err != nil {
        return oops.Wrapf(err, "something failed")
    }
    return nil
    
    // ✅ Do this instead:
    return oops.Wrapf(mayFail(), "something failed")
  5. Compare Standard Go errors vs. Oops error handling

    main

    When deciding between standard Go error handling and Oops, consider the following trade-offs:

    Standard Go Errors

    Best for: Simple, lightweight applications with no external dependencies where performance is critical and context requirements are minimal.

    • Pros: Minimal overhead, familiar syntax, no dependencies.
    • Cons: No structured attributes, manual stack trace management, difficult to propagate rich context across layers.

    Oops Error Handling

    Best for: Complex applications requiring rich contextual information, structured logging, and easy debugging.

    • Pros: Automatic stacktraces, fluent builder pattern, structured attributes (tags, codes, hints), and easy integration with loggers.
    • Cons: Adds an external dependency and is slightly more verbose than standard errors.
    // Standard Go Example
    func processUser(userID int) error {
        if userID <= 0 {
            return fmt.Errorf("invalid user ID: %d", userID)
        }
        
        if err := databaseOperation(userID); err != nil {
            return fmt.Errorf("failed to process user %d: %w", userID, err)
        }
        
        return nil
    }
    
    // Oops Example
    func processUser(userID int) error {
        if userID <= 0 {
            return oops.
                Code("invalid_user_id").
                In("user_processing").
                Tags("validation", "user").
                With("user_id", userID).
                Hint("User ID must be a positive integer").
                Errorf("invalid user ID: %d", userID)
        }
        
        if err := databaseOperation(userID); err != nil {
            return oops.
                In("user_processing").
                Tags("database", "user").
                With("user_id", userID).
                Wrapf(err, "failed to process user %d", userID)
        }
        
        return nil
    }
  6. Compare Oops with other error libraries

    main

    Oops provides a middle ground between simple error wrapping and heavy-duty error frameworks.

    vs. pkg/errors

    While pkg/errors provides stacktraces and wrapping, Oops offers more structured context, a built-in attributes system, logger integration, HTTP context support, and debugging hints.

    vs. github.com/cockroachdb/errors

    cockroachdb/errors is highly feature-rich for complex network error handling. Oops is preferred if you want a simpler API, better logger integration, and a focus on structured logging via a fluent builder pattern.

    vs. github.com/rotisserie/eris

    eris provides error wrapping with codes and stacktraces. Oops provides more structured context, HTTP context support, and debugging hints.

  7. How the Oops Error Builder pattern works

    main

    Oops uses a fluent builder pattern to construct rich, structured errors. Instead of creating a simple error with errors.New, you chain methods on the oops builder to attach context like error codes, domains, tags, and custom attributes. This allows you to build highly descriptive errors that are useful for both structured logging and debugging.

    Commonly used methods include:

    • Code(code any): Sets a machine-readable error code.
    • In(domain string): Sets the error domain.
    • Tags(tags ...string): Adds categorization tags.
    • With(kv ...any): Adds arbitrary key-value pairs as attributes.
    • Hint(hint string): Provides a human-readable hint for resolution.
    • Errorf(format string, args ...any): Finalizes the builder and returns the error.
    err := oops.
        Code("auth_failed").
        In("authentication").
        Tags("security", "auth").
        With("user_id", 123).
        Hint("Check user permissions").
        Errorf("authentication failed")
  8. Use the Oops builder pattern for rich error context

    main

    Oops uses a fluent builder pattern via *oops.OopsErrorBuilder. You can chain methods to attach metadata like error codes, domains, tags, traces, and spans before calling .Errorf() to finalize the error.

    err := oops.Code("auth_failed").In("authentication").Tags("security").Trace("trace-id").Errorf("authentication failed")
  9. Implement Error Code Standards

    main

    To maintain a predictable API and error handling logic, define error codes as constants. Use the oops.Code(string) method to attach these codes to errors. This allows consumers to programmatically identify the type of error (e.g., auth_failed, validation_failed) without parsing error strings.

    When creating errors, combine Code() with other context methods like In(), Tags(), and With() to provide a rich, structured error object.

    // Define error codes as constants
    const (
        ErrCodeAuthFailed       = "auth_failed"
        ErrCodeValidationFailed = "validation_failed"
        ErrCodeDatabaseError    = "database_error"
        ErrCodeNetworkTimeout   = "network_timeout"
        ErrCodeFileNotFound     = "file_not_found"
        ErrCodePermissionDenied = "permission_denied"
    )
    
    func authenticateUser(username, password string) error {
        if username == "" {
            return oops.
                Code(ErrCodeValidationFailed).
                In("authentication").
                Tags("auth", "validation").
                With("field", "username").
                Hint("Username is required").
                Errorf("missing username")
        }
        
        return oops.
            Code(ErrCodeAuthFailed).
            In("authentication").
            Tags("auth", "security").
            With("username", username).
            Hint("Check credentials").
            Public("Invalid username or password").
            Errorf("authentication failed for user %s", username)
    }
  10. Contribute to Oops

    main

    To contribute to the project, follow these steps:

    1. Fork the repository.
    2. Create a new feature branch.
    3. Implement your changes.
    4. Add corresponding tests to ensure stability.
    5. Submit a pull request for review.
  11. Configure stack trace behavior

    main

    The library provides detailed, annotated stack traces. You can customize how they are captured and filtered.

    • Max Depth: Control the depth of the stack trace using oops.StackTraceMaxDepth (default: 10).
    • Skip Frames: If you wrap oops in helper functions, use .CallerSkip(n int) to skip $N$ frames so the trace points to the actual caller.
    • Permanent Exclusions: Use oops.FrameSkip(pathSubstring, functionSubstring) to permanently exclude specific files or functions from all stack traces (call this at program startup).
    // Set max depth
    oops.StackTraceMaxDepth = 42
    
    // Skip frames in a helper
    func myWrapError(err error) error {
        return oops.CallerSkip(1).Wrap(err)
    }
    
    // Exclude specific patterns
    oops.FrameSkip("myproject/pkg/errutil", "")
    oops.FrameSkip("", "WrapErr")
  12. Integrate Oops with logging libraries

    main

    Oops is not a logger, but it is designed to work with them. When logging an Oops error, pass the entire error object to the logger to ensure the full context (stack traces, attributes, etc.) is captured, rather than just calling .Error().

    // Zap integration
    logger.Error("error occurred", zap.Any("error", err))
    
    // Zerolog integration
    logger.Error().Interface("error", err).Msg("auth error")
    
    // Logrus integration
    logger.WithField("error", err).Error("file operation failed")
    
    // Slog integration
    logger.Error("network error", slog.Any("error", err))