trogonerror

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2025 License: MIT Imports: 7 Imported by: 0

README

TrogonError

TrogonError is a Go library that brings structured, secure error handling to distributed systems based on the Straw Hat Error Specification.

TrogonError provides standardized error codes, rich metadata, and visibility controls to create consistent error handling across service boundaries. It goes beyond simple error messages to include contextual information, retry guidance, and internationalization support while implementing a three-tier visibility system to control error disclosure.

TrogonError addresses the critical challenge of inconsistent error handling in distributed systems, where different services often use incompatible error formats. It prevents information leakage through visibility controls, enables effective debugging with error chaining and metadata, and improves user experience with localized messages and help links.

TrogonError is particularly valuable for teams building microservices, REST APIs, or gRPC services that require consistent error responses. Enterprise development teams benefit from its secure error handling with controlled information disclosure, while DevOps and SRE teams leverage its rich metadata for debugging distributed systems.

TrogonError provides structured error handling with:

  • Standardized error codes mapped to common failure scenarios
  • Rich metadata for debugging and error correlation
  • Three-tier visibility system (Internal/Private/Public) for secure information disclosure
  • Error chaining and stack traces for comprehensive debugging
  • Internationalization support for user-facing error messages
  • Retry guidance with duration or absolute time specifications
  • Help links for enhanced user experience
  • Template system for consistent error definitions across services

Getting Started

Installation
go get github.com/TrogonStack/trogonerror

For production applications, use error templates to ensure consistency and maintainability. You may define platform-specific error templates in a separate package and import them into your application. As well as define your own error templates for your application.

import (
    "context"
    "github.com/TrogonStack/trogonerror"
)

// Define reusable error templates
var (
    ErrUserNotFound = trogonerror.NewErrorTemplate("shopify.users", "NOT_FOUND",
        trogonerror.TemplateWithCode(trogonerror.CodeNotFound))

    ErrValidationFailed = trogonerror.NewErrorTemplate("shopify", "VALIDATION_FAILED",
        trogonerror.TemplateWithCode(trogonerror.CodeInvalidArgument))

    ErrOrderNotCancellable = trogonerror.NewErrorTemplate("shopify.orders", "ORDER_NOT_CANCELLABLE",
        trogonerror.TemplateWithCode(trogonerror.CodeFailedPrecondition))

    ErrDatabaseError = trogonerror.NewErrorTemplate("shopify", "DATABASE_ERROR",
        trogonerror.TemplateWithCode(trogonerror.CodeInternal))
)

type GetUser struct {
    UserID string
}
func GetUserHandler(ctx context.Context, cmd GetUser) (*User, error) {
    user, err := db.FindUser(ctx, "SELECT * FROM users WHERE id = $1", cmd.UserID)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            // This is a common pattern for not found errors
            return nil, ErrUserNotFound.NewError(
                trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/%s", cmd.UserID))
        }

        // This is a common pattern for database errors
        return nil, ErrDatabaseError.NewError(
            trogonerror.WithWrap(err))
    }

    // ... rest of the logic
}

type CreateUser struct {
    Email    string
}
func CreateUserHandler(ctx context.Context, cmd CreateUser) (*User, error) {
    if cmd.Email == "" {
        // This is a common pattern for validation errors
        return nil, ErrValidationFailed.NewError(
            trogonerror.WithSubject("/email"),
            trogonerror.WithMessage("email is required"),
            trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "validationType", "REQUIRED"))
    }

    // ... rest of the logic
}


type CancelOrder struct {
    OrderID    string
}
func CancelOrderHandler(ctx context.Context, cmd CancelOrder) error {
    order, err := db.FindOrder(ctx, "SELECT * FROM orders WHERE id = $1", cmd.OrderID)
    // ... rest of the logic

    if order.Status == "delivered" {
        // This is a common pattern for failed precondition errors
        return ErrOrderNotCancellable.NewError(
            trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/%s", cmd.OrderID),
            trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "reason", "ALREADY_DELIVERED"),
            trogonerror.WithHelpLinkf("Order Management", "https://admin.shopify.com/orders/%s", cmd.OrderID))
    }

    // ... rest of the logic
}
Basic Usage without Template Errors
import "github.com/TrogonStack/trogonerror"

func main() {
    // Create a simple error with clear domain and uppercase reason
    err := trogonerror.NewError("shopify.users", "NOT_FOUND",
        trogonerror.WithCode(trogonerror.CodeNotFound),
        trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/1234567890"),
        trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "requestedBy", "storefront-api"),
        trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "shopId", "mystore.myshopify.com"))

    fmt.Println(err.Error())   // "resource not found"
    fmt.Println(err.Domain())  // "shopify.users"
    fmt.Println(err.Reason())  // "NOT_FOUND"
}

Documentation

Overview

Package trogonerror provides a comprehensive, standardized error handling system for distributed applications based on the TrogonError specification.

TrogonError offers structured error handling with standardized error codes, rich metadata, visibility controls, internationalization support, and cause chaining for better debugging and error correlation.

For production applications, use error templates to define reusable error patterns. Templates ensure consistency across your application and reduce code duplication:

var (
	ErrUserNotFound = trogonerror.NewErrorTemplate("shopify.users", "NOT_FOUND",
		trogonerror.TemplateWithCode(trogonerror.CodeNotFound))

	ErrValidationFailed = trogonerror.NewErrorTemplate("shopify.validation", "INVALID_INPUT",
		trogonerror.TemplateWithCode(trogonerror.CodeInvalidArgument))

	ErrDatabaseError = trogonerror.NewErrorTemplate("shopify.database", "CONNECTION_FAILED",
		trogonerror.TemplateWithCode(trogonerror.CodeInternal))
)

func GetUser(id string) (*User, error) {
	user, err := db.FindUser(id)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrUserNotFound.NewError(
				trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/%s", id))
		}
		return nil, ErrDatabaseError.NewError(trogonerror.WithWrap(err))
	}
	return user, nil
}

Basic Error Creation

Create errors using functional options:

err := trogonerror.NewError("shopify.users", "NOT_FOUND",
	trogonerror.WithCode(trogonerror.CodeNotFound),
	trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/1234567890"))

Error Codes and HTTP Status Mapping

TrogonError supports 16 standardized error codes that map to HTTP status codes:

Code                    HTTP Status    Description
----                    -----------    -----------
Cancelled               499           Request cancelled
Unknown                 500           Unknown error
InvalidArgument         400           Invalid request parameters
DeadlineExceeded        504           Request timeout
NotFound                404           Resource not found
AlreadyExists           409           Resource already exists
PermissionDenied        403           Access denied
Unauthenticated         401           Authentication required
ResourceExhausted       429           Rate limit exceeded
FailedPrecondition      400           Precondition failed
Aborted                 409           Operation aborted
OutOfRange              400           Value out of range
Unimplemented           501           Not implemented
Internal                500           Internal server error
Unavailable             503           Service unavailable
DataLoss                500           Data corruption

Access code information:

code := trogonerror.CodeNotFound
fmt.Println(code.String())         // "NOT_FOUND"
fmt.Println(code.HttpStatusCode()) // 404
fmt.Println(code.Message())        // "resource not found"

Visibility Control System

TrogonError implements a three-tier visibility system to control information disclosure:

VisibilityInternal: Only visible within the same service/process
VisibilityPrivate:  Visible across internal services (not to external users)
VisibilityPublic:   Safe to expose to external users

err := trogonerror.NewError("shopify.auth", "ACCESS_DENIED",
	trogonerror.WithCode(trogonerror.CodePermissionDenied),
	trogonerror.WithVisibility(trogonerror.VisibilityPublic),
	trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "userId", "gid://shopify/Customer/1234567890"),
	trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "resource", "/admin/customers"),
	trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "action", "DELETE"))

Rich Metadata with Formatting

Add structured metadata with visibility controls and printf-style formatting:

err := trogonerror.NewError("shopify.orders", "ORDER_PROCESSING_FAILED",
	trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/5432109876"),
	trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "customerId", "gid://shopify/Customer/%s", userID),
	trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "amount", "$%.2f", float64(amount)/100))

Error Chaining and Wrapping

Chain errors to preserve context while wrapping standard Go errors:

dbErr := trogonerror.NewError("shopify.database", "CONNECTION_FAILED",
	trogonerror.WithCode(trogonerror.CodeInternal))

serviceErr := trogonerror.NewError("shopify.users", "USER_FETCH_FAILED",
	trogonerror.WithCode(trogonerror.CodeInternal),
	trogonerror.WithCause(dbErr))

// Wrap standard Go errors
originalErr := fmt.Errorf("connection timeout")
wrappedErr := trogonerror.NewError("shopify.database", "CONNECTION_TIMEOUT",
	trogonerror.WithCode(trogonerror.CodeUnavailable),
	trogonerror.WithWrap(originalErr),
	trogonerror.WithErrorMessage(originalErr))

Debugging Support

Capture stack traces and debug information for internal debugging:

err := trogonerror.NewError("shopify.database", "QUERY_TIMEOUT",
	trogonerror.WithCode(trogonerror.CodeInternal),
	trogonerror.WithStackTrace(),
	trogonerror.WithDebugDetail("Query execution exceeded 30s timeout"),
	trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "query", "SELECT * FROM users WHERE id = $1"))

// Access debug information
if debugInfo := err.DebugInfo(); debugInfo != nil {
	fmt.Println("Detail:", debugInfo.Detail())
	fmt.Println("Stack entries:", len(debugInfo.StackEntries()))
}

Retry Guidance

Provide retry guidance with duration offset or absolute time:

// Retry after a duration
err := trogonerror.NewError("shopify.api", "RATE_LIMIT_EXCEEDED",
	trogonerror.WithCode(trogonerror.CodeResourceExhausted),
	trogonerror.WithRetryInfoDuration(60*time.Second),
	trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "limit", "1000"))

// Retry at specific time
retryTime := time.Now().Add(5 * time.Minute)
err := trogonerror.NewError("shopify.maintenance", "SERVICE_UNAVAILABLE",
	trogonerror.WithCode(trogonerror.CodeUnavailable),
	trogonerror.WithRetryTime(retryTime))

Provide actionable help links with formatting support:

err := trogonerror.NewError("shopify.users", "INVALID_EMAIL",
	trogonerror.WithCode(trogonerror.CodeInvalidArgument),
	trogonerror.WithHelpLink("Fix Email", "https://admin.shopify.com/customers/1234567890/edit#email"),
	trogonerror.WithHelpLinkf("Customer Console", "https://admin.shopify.com/customers/%s/help", userID))

Internationalization

Support localized error messages:

err := trogonerror.NewError("shopify.users", "NOT_FOUND",
	trogonerror.WithCode(trogonerror.CodeNotFound),
	trogonerror.WithLocalizedMessage("es-ES", "Usuario no encontrado"))

fmt.Println(err.Message())                    // "resource not found" (default)
fmt.Println(err.LocalizedMessage().Message()) // "Usuario no encontrado"

Error Mutation with WithChanges

Create modified copies of errors efficiently:

original := trogonerror.NewError("shopify.orders", "ORDER_FAILED",
	trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/12345"))

modified := original.WithChanges(
	trogonerror.WithChangeID("error-123"),
	trogonerror.WithChangeSourceID("payment-service"),
	trogonerror.WithChangeMetadataValuef(trogonerror.VisibilityPublic, "customerId", "gid://shopify/Customer/%s", userID))

Standard Go Error Compatibility

TrogonError implements the standard Go error interface and works with errors.Is, errors.As, and error wrapping:

// Type assertion
if tErr, ok := err.(*trogonerror.TrogonError); ok {
	fmt.Printf("Domain: %s, Reason: %s\n", tErr.Domain(), tErr.Reason())
}

// errors.Is comparison (by domain + reason)
if errors.Is(err, ErrUserNotFound.NewError()) {
	fmt.Println("User not found")
}

// errors.As type assertion
var tErr *trogonerror.TrogonError
if errors.As(err, &tErr) {
	fmt.Printf("Code: %s, HTTP Status: %d\n", tErr.Code().String(), tErr.Code().HttpStatusCode())
}

Template-Based Error Checking

Use template.Is() for efficient error type checking:

if ErrUserNotFound.Is(err) {
	fmt.Println("This is a user not found error")
}

Complete Example

Here's a complete example showing production usage:

var ErrPaymentFailed = trogonerror.NewErrorTemplate("shopify.payments", "PAYMENT_DECLINED",
	trogonerror.TemplateWithCode(trogonerror.CodeInternal))

func ProcessPayment(orderID, userID string, amount int) error {
	// Simulate payment processing
	if amount > 100000 { // Over $1000
		return ErrPaymentFailed.NewError(
			trogonerror.WithMessage("Payment amount exceeds limit"),
			trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/%s", orderID),
			trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "customerId", "gid://shopify/Customer/%s", userID),
			trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "amount", "$%.2f", float64(amount)/100),
			trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "currency", "USD"),
			trogonerror.WithHelpLinkf("Contact Support", "https://admin.shopify.com/support/new?order_id=%s", orderID),
			trogonerror.WithRetryInfoDuration(30*time.Minute))
	}
	return nil
}

Index

Examples

Constants

View Source
const SpecVersion = 1

SpecVersion represents the version of the error specification

Variables

This section is empty.

Functions

This section is empty.

Types

type ChangeOption

type ChangeOption func(*TrogonError)

ChangeOption represents a change to apply to a TrogonError

func WithChangeHelpLink(description, url string) ChangeOption

WithChangeHelpLink adds a help link with a static URL (appends to existing help). Use WithChangeHelpLinkf for URLs that need parameter interpolation.

func WithChangeHelpLinkf

func WithChangeHelpLinkf(description, urlFormat string, args ...any) ChangeOption

WithChangeHelpLinkf adds a help link with printf-style formatting for the URL (appends to existing help). Example: WithChangeHelpLinkf("Order Details", "https://console.myapp.com/orders/%s", orderID)

func WithChangeID

func WithChangeID(id string) ChangeOption

WithChangeID sets the error ID

func WithChangeLocalizedMessage

func WithChangeLocalizedMessage(locale, message string) ChangeOption

WithChangeLocalizedMessage sets localized message (replaces existing)

func WithChangeMetadata

func WithChangeMetadata(metadata map[string]MetadataValue) ChangeOption

WithChangeMetadata sets metadata with explicit visibility control

func WithChangeMetadataValue

func WithChangeMetadataValue(visibility Visibility, key, value string) ChangeOption

WithChangeMetadataValue sets a single metadata entry with specific visibility

func WithChangeMetadataValuef

func WithChangeMetadataValuef(visibility Visibility, key, valueFormat string, args ...any) ChangeOption

WithChangeMetadataValuef sets a single metadata entry with printf-style formatting for the value Example: WithChangeMetadataValuef(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/%s", orderID)

func WithChangeRetryInfoDuration

func WithChangeRetryInfoDuration(retryOffset time.Duration) ChangeOption

WithChangeRetryInfoDuration sets retry duration (replaces existing retry info)

func WithChangeRetryTime

func WithChangeRetryTime(retryTime time.Time) ChangeOption

WithChangeRetryTime sets absolute retry time (replaces existing retry info)

func WithChangeSourceID

func WithChangeSourceID(sourceID string) ChangeOption

WithChangeSourceID sets the source ID

func WithChangeTime

func WithChangeTime(timestamp time.Time) ChangeOption

WithChangeTime sets the timestamp

type Code

type Code int

Code represents standardized error codes that map to HTTP status codes

Example (Methods)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	code := trogonerror.CodeNotFound

	fmt.Println(code.String())
	fmt.Println(code.HttpStatusCode())
}
Output:

NOT_FOUND
404
Example (Utilities)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Working with error codes
	code := trogonerror.CodeNotFound

	fmt.Printf("Code string: %s\n", code.String())
	fmt.Printf("HTTP status: %d\n", code.HttpStatusCode())
	fmt.Printf("Default message: %s\n", code.Message())

	err := trogonerror.NewError("shopify.core", "INVALID_REQUEST", trogonerror.WithCode(trogonerror.CodeInvalidArgument))
	if err.Code() == trogonerror.CodeInvalidArgument {
		fmt.Println("This is an invalid argument error")
	}

}
Output:

Code string: NOT_FOUND
HTTP status: 404
Default message: resource not found
This is an invalid argument error
const (
	CodeCancelled Code = 1 + iota
	CodeUnknown
	CodeInvalidArgument
	CodeDeadlineExceeded
	CodeNotFound
	CodeAlreadyExists
	CodePermissionDenied
	CodeResourceExhausted
	CodeFailedPrecondition
	CodeAborted
	CodeOutOfRange
	CodeUnimplemented
	CodeInternal
	CodeUnavailable
	CodeDataLoss
	CodeUnauthenticated
)

func (Code) HttpStatusCode

func (c Code) HttpStatusCode() int

func (Code) Message

func (c Code) Message() string

func (Code) String

func (c Code) String() string

type DebugInfo

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

DebugInfo contains technical details for internal debugging

func (DebugInfo) Detail

func (d DebugInfo) Detail() string

func (DebugInfo) StackEntries

func (d DebugInfo) StackEntries() []string

StackEntries converts the runtime.Frame objects to formatted strings

func (DebugInfo) StackFrames

func (d DebugInfo) StackFrames() []runtime.Frame

StackFrames returns the raw runtime.Frame objects for advanced use cases

type ErrorOption

type ErrorOption func(*TrogonError)

ErrorOption represents options for error construction

func WithCause

func WithCause(causes ...*TrogonError) ErrorOption

WithCause adds one or more causes to the error

Example
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	dbErr := trogonerror.NewError("shopify.database", "CONNECTION_FAILED",
		trogonerror.WithCode(trogonerror.CodeInternal),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "host", "postgres-primary.shopify.com"))

	serviceErr := trogonerror.NewError("shopify.users", "USER_FETCH_FAILED",
		trogonerror.WithCode(trogonerror.CodeInternal),
		trogonerror.WithMessage("Failed to fetch user data"),
		trogonerror.WithCause(dbErr))

	fmt.Println(serviceErr.Error())
	fmt.Println(len(serviceErr.Causes()))
	fmt.Println(serviceErr.Causes()[0].Domain())
}
Output:

Failed to fetch user data
  visibility: INTERNAL
  domain: shopify.users
  reason: USER_FETCH_FAILED
  code: INTERNAL
1
shopify.database
Example (ErrorChaining)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	dbErr := trogonerror.NewError("shopify.database", "CONNECTION_FAILED",
		trogonerror.WithCode(trogonerror.CodeUnavailable),
		trogonerror.WithMessage("Database connection timeout"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "host", "postgres-primary.shopify.com"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "port", "5432"))

	serviceErr := trogonerror.NewError("shopify.users", "USER_FETCH_FAILED",
		trogonerror.WithCode(trogonerror.CodeInternal),
		trogonerror.WithMessage("Failed to fetch user data"),
		trogonerror.WithCause(dbErr))

	fmt.Printf("Service error: %s\n", serviceErr.Reason())
	fmt.Printf("Has causes: %v\n", len(serviceErr.Causes()) > 0)
	if len(serviceErr.Causes()) > 0 {
		fmt.Printf("Root cause: %s\n", serviceErr.Causes()[0].Reason())
		fmt.Printf("Root cause domain: %s\n", serviceErr.Causes()[0].Domain())
	}

}
Output:

Service error: USER_FETCH_FAILED
Has causes: true
Root cause: CONNECTION_FAILED
Root cause domain: shopify.database

func WithCode

func WithCode(code Code) ErrorOption

WithCode sets the error code

func WithDebugDetail

func WithDebugDetail(detail string) ErrorOption

WithDebugDetail sets debug detail message without capturing stack trace

func WithDebugInfo

func WithDebugInfo(debugInfo DebugInfo) ErrorOption

WithDebugInfo sets debug information (for internal use only)

func WithErrorMessage

func WithErrorMessage(err error) ErrorOption

WithErrorMessage sets the error message to the error's Error() string

func WithHelp

func WithHelp(help Help) ErrorOption

WithHelp sets the help information

func WithHelpLink(description, url string) ErrorOption

WithHelpLink adds a help link with a static URL. Use WithHelpLinkf for URLs that need parameter interpolation.

func WithHelpLinkf

func WithHelpLinkf(description, urlFormat string, args ...any) ErrorOption

WithHelpLinkf adds a help link with printf-style formatting for the URL. Example: WithHelpLinkf("User Console", "https://console.myapp.com/users/%s", userID)

func WithID

func WithID(id string) ErrorOption

WithID sets the error ID

func WithLocalizedMessage

func WithLocalizedMessage(locale, message string) ErrorOption

WithLocalizedMessage sets localized message

Example (Internationalization)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	err := trogonerror.NewError("shopify.users", "NOT_FOUND",
		trogonerror.WithCode(trogonerror.CodeNotFound),
		trogonerror.WithLocalizedMessage("es-ES", "Usuario no encontrado"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/7890123456"))

	fmt.Printf("Default message: %s\n", err.Message())
	if localizedMsg := err.LocalizedMessage(); localizedMsg != nil {
		fmt.Printf("Localized (%s): %s\n", localizedMsg.Locale(), localizedMsg.Message())
	}

}
Output:

Default message: resource not found
Localized (es-ES): Usuario no encontrado

func WithMessage

func WithMessage(message string) ErrorOption

WithMessage sets the error message

func WithMetadata

func WithMetadata(metadata map[string]MetadataValue) ErrorOption

WithMetadata sets metadata with explicit visibility control

func WithMetadataValue

func WithMetadataValue(visibility Visibility, key, value string) ErrorOption

WithMetadataValue sets a single metadata entry with specific visibility

func WithMetadataValuef

func WithMetadataValuef(visibility Visibility, key, valueFormat string, args ...any) ErrorOption

WithMetadataValuef sets a single metadata entry with printf-style formatting for the value Example: WithMetadataValuef(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/%s", orderID)

Example (FormattedValues)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Using printf-style formatting for metadata values
	userID := "1234567890"
	orderID := "5432109876"
	amount := 29999 // cents

	err := trogonerror.NewError("shopify.orders", "ORDER_PROCESSING_FAILED",
		trogonerror.WithCode(trogonerror.CodeInternal),
		trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "customerId", "gid://shopify/Customer/%s", userID),
		trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "orderId", "gid://shopify/Order/%s", orderID),
		trogonerror.WithMetadataValuef(trogonerror.VisibilityPublic, "amount", "$%.2f", float64(amount)/100),
		trogonerror.WithMetadataValuef(trogonerror.VisibilityInternal, "requestId", "req_%s_%s", userID, orderID))

	fmt.Printf("Customer ID: %s\n", err.Metadata()["customerId"].Value())
	fmt.Printf("Order ID: %s\n", err.Metadata()["orderId"].Value())
	fmt.Printf("Amount: %s\n", err.Metadata()["amount"].Value())
	fmt.Printf("Request ID: %s\n", err.Metadata()["requestId"].Value())

}
Output:

Customer ID: gid://shopify/Customer/1234567890
Order ID: gid://shopify/Order/5432109876
Amount: $299.99
Request ID: req_1234567890_5432109876

func WithRetryInfoDuration

func WithRetryInfoDuration(retryOffset time.Duration) ErrorOption

WithRetryInfoDuration sets retry information with a duration offset Following ADR: servers MUST set either retry_offset OR retry_time, never both

Example (RetryLogic)
package main

import (
	"fmt"
	"time"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Error with retry information for rate limiting
	err := trogonerror.NewError("shopify.api", "RATE_LIMIT_EXCEEDED",
		trogonerror.WithCode(trogonerror.CodeResourceExhausted),
		trogonerror.WithMessage("API rate limit exceeded"),
		trogonerror.WithRetryInfoDuration(60*time.Second),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "limit", "100"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "timeWindow", "1m"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "remaining", "0"))

	if retryInfo := err.RetryInfo(); retryInfo != nil {
		if retryOffset := retryInfo.RetryOffset(); retryOffset != nil {
			fmt.Printf("Retry after: %s\n", retryOffset.String())
		}
	}
	fmt.Printf("Rate limit: %s\n", err.Metadata()["limit"].Value())
	fmt.Printf("Window: %s\n", err.Metadata()["timeWindow"].Value())

}
Output:

Retry after: 1m0s
Rate limit: 100
Window: 1m

func WithRetryTime

func WithRetryTime(retryTime time.Time) ErrorOption

WithRetryTime sets retry information with an absolute time Following ADR: servers MUST set either retry_offset OR retry_time, never both

Example (AbsoluteRetry)
package main

import (
	"fmt"
	"time"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	retryTime := time.Date(2024, 1, 15, 14, 35, 0, 0, time.UTC)

	err := trogonerror.NewError("shopify.maintenance", "SERVICE_UNAVAILABLE",
		trogonerror.WithCode(trogonerror.CodeUnavailable),
		trogonerror.WithMessage("Service temporarily unavailable for maintenance"),
		trogonerror.WithRetryTime(retryTime),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "maintenanceWindow", "30min"))

	if retryInfo := err.RetryInfo(); retryInfo != nil {
		if retryTime := retryInfo.RetryTime(); retryTime != nil {
			fmt.Printf("Retry at: %s\n", retryTime.Format("2006-01-02 15:04:05 UTC"))
		}
	}
	fmt.Printf("Maintenance window: %s\n", err.Metadata()["maintenanceWindow"].Value())

}
Output:

Retry at: 2024-01-15 14:35:00 UTC
Maintenance window: 30min

func WithSourceID

func WithSourceID(sourceID string) ErrorOption

WithSourceID sets the source ID

func WithStackTrace

func WithStackTrace() ErrorOption

WithStackTrace annotates the error with a stack trace at the point WithStackTrace was called This captures the current call stack for debugging purposes (internal use only)

Example (Debugging)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Error with stack trace for debugging
	err := trogonerror.NewError("shopify.database", "QUERY_TIMEOUT",
		trogonerror.WithCode(trogonerror.CodeInternal),
		trogonerror.WithStackTrace(),
		trogonerror.WithDebugDetail("Database query failed with timeout"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "query", "SELECT * FROM customers WHERE id = $1"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "requestId", "req_2024_01_15_db_query_abc123"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "duration", "1.5s"))

	fmt.Printf("Has debug info: %v\n", err.DebugInfo() != nil)
	if err.DebugInfo() != nil {
		fmt.Printf("Debug detail: %s\n", err.DebugInfo().Detail())
		fmt.Printf("Stack entries count: %d\n", len(err.DebugInfo().StackEntries()))
	}

}
Output:

Has debug info: true
Debug detail: Database query failed with timeout
Stack entries count: 9

func WithStackTraceDepth

func WithStackTraceDepth(maxDepth int) ErrorOption

WithStackTraceDepth annotates the error with a stack trace up to the specified depth

func WithSubject

func WithSubject(subject string) ErrorOption

WithSubject sets the error subject

func WithTime

func WithTime(timestamp time.Time) ErrorOption

WithTime sets the error timestamp

func WithVisibility

func WithVisibility(visibility Visibility) ErrorOption

WithVisibility sets the error visibility

func WithWrap

func WithWrap(err error) ErrorOption

WithWrap wraps an existing error

Example (StandardErrors)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	originalErr := fmt.Errorf("connection timeout after 30s")

	wrappedErr := trogonerror.NewError("shopify.database", "CONNECTION_TIMEOUT",
		trogonerror.WithCode(trogonerror.CodeUnavailable),
		trogonerror.WithWrap(originalErr),
		trogonerror.WithErrorMessage(originalErr),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "timeout", "30s"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "host", "postgres-primary.shopify.com"))

	fmt.Printf("Wrapped error domain: %s\n", wrappedErr.Domain())
	fmt.Printf("Wrapped error reason: %s\n", wrappedErr.Reason())
	fmt.Printf("Original message preserved: %v\n", wrappedErr.Message() == originalErr.Error())

}
Output:

Wrapped error domain: shopify.database
Wrapped error reason: CONNECTION_TIMEOUT
Original message preserved: true

type ErrorTemplate

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

ErrorTemplate represents a reusable error definition

Example
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	template := trogonerror.NewErrorTemplate("shopify.users", "NOT_FOUND",
		trogonerror.TemplateWithCode(trogonerror.CodeNotFound))

	err := template.NewError(
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/1234567890"))

	fmt.Println(err.Error())
	fmt.Println(err.Domain())
	fmt.Println(err.Reason())
}
Output:

resource not found
  visibility: INTERNAL
  domain: shopify.users
  reason: NOT_FOUND
  code: NOT_FOUND
  metadata:
    - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC
shopify.users
NOT_FOUND
Example (Production)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	var (
		ErrUserNotFound = trogonerror.NewErrorTemplate("shopify.users", "NOT_FOUND",
			trogonerror.TemplateWithCode(trogonerror.CodeNotFound),
			trogonerror.TemplateWithCode(trogonerror.CodeNotFound))

		ErrInvalidInput = trogonerror.NewErrorTemplate("shopify.validation", "INVALID_INPUT",
			trogonerror.TemplateWithCode(trogonerror.CodeInvalidArgument))
	)

	userErr := ErrUserNotFound.NewError(
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/4567890123"))

	inputErr := ErrInvalidInput.NewError(
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "fieldName", "email"),
		trogonerror.WithSubject("/email"))

	fmt.Println("User error:", userErr.Domain(), userErr.Reason())
	fmt.Println("Input error:", inputErr.Domain(), inputErr.Reason())

}
Output:

User error: shopify.users NOT_FOUND
Input error: shopify.validation INVALID_INPUT
Example (Reusable)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Create a validation error template
	validationTemplate := trogonerror.NewErrorTemplate("shopify.validation", "FIELD_INVALID",
		trogonerror.TemplateWithCode(trogonerror.CodeInvalidArgument),
		trogonerror.TemplateWithVisibility(trogonerror.VisibilityPublic))

	// Create multiple validation errors from the same template
	emailErr := validationTemplate.NewError(
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "fieldName", "email"),
		trogonerror.WithSubject("/email"))

	phoneErr := validationTemplate.NewError(
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "fieldName", "phone"),
		trogonerror.WithSubject("/phone"))

	fmt.Println("Email validation:", emailErr.Error())
	fmt.Println("Phone validation:", phoneErr.Error())
	fmt.Println("Same domain:", emailErr.Domain() == phoneErr.Domain())
	fmt.Println("Same reason:", emailErr.Reason() == phoneErr.Reason())

}
Output:

Email validation: invalid argument provided
  visibility: PUBLIC
  domain: shopify.validation
  reason: FIELD_INVALID
  code: INVALID_ARGUMENT
  subject: /email
  metadata:
    - fieldName: email visibility=PUBLIC
Phone validation: invalid argument provided
  visibility: PUBLIC
  domain: shopify.validation
  reason: FIELD_INVALID
  code: INVALID_ARGUMENT
  subject: /phone
  metadata:
    - fieldName: phone visibility=PUBLIC
Same domain: true
Same reason: true

func NewErrorTemplate

func NewErrorTemplate(domain, reason string, options ...TemplateOption) *ErrorTemplate

NewErrorTemplate creates a reusable error template for consistent error creation.

func (*ErrorTemplate) Is

func (et *ErrorTemplate) Is(target error) bool

Is checks if the given error matches this template's domain and reason This allows checking if an error was created from this template without requiring the template to implement the error interface

func (*ErrorTemplate) NewError

func (et *ErrorTemplate) NewError(options ...ErrorOption) *TrogonError

NewError creates a new error instance from the template

type Help

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

Help provides links to relevant documentation

func (h Help) Links() []HelpLink
type HelpLink struct {
	// contains filtered or unexported fields
}

HelpLink provides documentation link

func (HelpLink) Description

func (h HelpLink) Description() string

func (HelpLink) URL

func (h HelpLink) URL() string

type LocalizedMessage

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

LocalizedMessage provides translated error message

func (LocalizedMessage) Locale

func (l LocalizedMessage) Locale() string

func (LocalizedMessage) Message

func (l LocalizedMessage) Message() string

type Metadata

type Metadata = map[string]MetadataValue

Metadata represents a map of metadata with visibility control

type MetadataValue

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

MetadataValue contains both the value and its visibility level

func (MetadataValue) Value

func (m MetadataValue) Value() string

func (MetadataValue) Visibility

func (m MetadataValue) Visibility() Visibility

type RetryInfo

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

RetryInfo describes when a client can retry a failed request Following ADR requirements: servers MUST set either retry_offset OR retry_time, never both

func (RetryInfo) RetryOffset

func (r RetryInfo) RetryOffset() *time.Duration

func (RetryInfo) RetryTime

func (r RetryInfo) RetryTime() *time.Time

type TemplateOption

type TemplateOption func(*ErrorTemplate)

TemplateOption represents options that can be applied to ErrorTemplate

func TemplateWithCode

func TemplateWithCode(code Code) TemplateOption

Template option functions

func TemplateWithHelp

func TemplateWithHelp(help Help) TemplateOption
func TemplateWithHelpLink(description, url string) TemplateOption

func TemplateWithMessage

func TemplateWithMessage(message string) TemplateOption

func TemplateWithVisibility

func TemplateWithVisibility(visibility Visibility) TemplateOption

type TrogonError

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

TrogonError represents the standardized error format following the ADR

Example (VisibilityControl)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Demonstrate visibility controls
	err := trogonerror.NewError("shopify.auth", "ACCESS_DENIED",
		trogonerror.WithCode(trogonerror.CodePermissionDenied),
		trogonerror.WithVisibility(trogonerror.VisibilityPublic),
		trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "userId", "gid://shopify/Customer/1234567890"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "resource", "/admin/customers"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "action", "DELETE"))

	fmt.Printf("Error visibility: %s\n", err.Visibility().String())

	// Show metadata with different visibility levels in alphabetical order
	metadataKeys := []string{"action", "resource", "userId"}
	for _, key := range metadataKeys {
		if value, exists := err.Metadata()[key]; exists {
			fmt.Printf("Metadata %s (%s): %s\n", key, value.Visibility().String(), value.Value())
		}
	}

}
Output:

Error visibility: PUBLIC
Metadata action (PUBLIC): DELETE
Metadata resource (PRIVATE): /admin/customers
Metadata userId (INTERNAL): gid://shopify/Customer/1234567890

func As added in v0.4.0

func As(err error, target trogonError) (*TrogonError, bool)

As checks if the error matches the target and returns the TrogonError if it does. This combines error matching and error extraction in a single, more idiomatic operation. The target can be either a TrogonError or an ErrorTemplate. Returns the TrogonError and true if the error matches, nil and false otherwise.

Example usage:

if trogonErr, ok := trogonerror.As(err, users.ErrUserNotFound); ok {
    return trogonErr.WithChanges(
        trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "user_id", req.UserID),
    )
}
Example (IdiomaticErrorHandling)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	// Define error templates (typically done at package level)
	var ErrInsufficientStock = trogonerror.NewErrorTemplate("inventory", "INSUFFICIENT_STOCK",
		trogonerror.TemplateWithCode(trogonerror.CodeResourceExhausted))

	// Simulate an error from inventory service
	inventoryErr := ErrInsufficientStock.NewError(
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "product_id", "prod_12345"))

	// Old verbose pattern:
	// if inventory.ErrInsufficientStock.Is(err) {
	//     var trogonErr *trogonerror.TrogonError
	//     if errors.As(err, &trogonErr) {
	//         return nil, trogonErr.WithChanges(...)
	//     }
	// }

	// New idiomatic pattern using As:
	if trogonErr, ok := trogonerror.As(inventoryErr, ErrInsufficientStock); ok {
		modifiedErr := trogonErr.WithChanges(
			trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "order_id", "order_789"),
			trogonerror.WithChangeMetadataValue(trogonerror.VisibilityPublic, "requested_quantity", "10"),
		)
		fmt.Printf("Modified error domain: %s\n", modifiedErr.Domain())
		fmt.Printf("Order ID: %s\n", modifiedErr.Metadata()["order_id"].Value())
		fmt.Printf("Requested quantity: %s\n", modifiedErr.Metadata()["requested_quantity"].Value())
	}

}
Output:

Modified error domain: inventory
Order ID: order_789
Requested quantity: 10

func NewError

func NewError(domain, reason string, options ...ErrorOption) *TrogonError

NewError creates a new TrogonError following the ADR specification. Domain should be a simple identifier like "myapp.users" (not reversed-DNS). Reason should be an UPPERCASE identifier like "NOT_FOUND".

Example
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	err := trogonerror.NewError("shopify.users", "NOT_FOUND",
		trogonerror.WithCode(trogonerror.CodeNotFound),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/1234567890"))

	fmt.Println(err.Error())
	fmt.Println(err.Domain())
	fmt.Println(err.Reason())
}
Output:

resource not found
  visibility: INTERNAL
  domain: shopify.users
  reason: NOT_FOUND
  code: NOT_FOUND
  metadata:
    - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC
shopify.users
NOT_FOUND
Example (Basic)
package main

import (
	"fmt"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	err := trogonerror.NewError("shopify.users", "NOT_FOUND",
		trogonerror.WithCode(trogonerror.CodeNotFound),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "userId", "gid://shopify/Customer/1234567890"))

	fmt.Println("Error message:", err.Error())
	fmt.Println("Domain:", err.Domain())
	fmt.Println("Reason:", err.Reason())
	fmt.Println("Code:", err.Code().String())

}
Output:

Error message: resource not found
  visibility: INTERNAL
  domain: shopify.users
  reason: NOT_FOUND
  code: NOT_FOUND
  metadata:
    - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC
Domain: shopify.users
Reason: NOT_FOUND
Code: NOT_FOUND
Example (RichMetadata)
package main

import (
	"fmt"
	"time"

	"github.com/TrogonStack/trogonerror"
)

func main() {
	err := trogonerror.NewError("shopify.payments", "PAYMENT_DECLINED",
		trogonerror.WithCode(trogonerror.CodeInternal),
		trogonerror.WithMessage("Payment processing failed due to upstream service error"),
		trogonerror.WithVisibility(trogonerror.VisibilityPrivate),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "paymentId", "pay_2024_01_15_abc123def456"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "amount", "299.99"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "currency", "USD"),
		trogonerror.WithMetadataValue(trogonerror.VisibilityInternal, "merchantId", "gid://shopify/Shop/1234567890"),
		trogonerror.WithSubject("/payment/amount"),
		trogonerror.WithTime(time.Date(2024, 1, 15, 14, 30, 45, 0, time.UTC)),
		trogonerror.WithSourceID("payment-gateway-prod-01"))

	fmt.Printf("Payment ID: %s\n", err.Metadata()["paymentId"].Value())
	fmt.Printf("Currency: %s\n", err.Metadata()["currency"].Value())
	fmt.Printf("Subject: %s\n", err.Subject())

}
Output:

Payment ID: pay_2024_01_15_abc123def456
Currency: USD
Subject: /payment/amount

func (TrogonError) Causes

func (e TrogonError) Causes() []*TrogonError

func (TrogonError) Code

func (e TrogonError) Code() Code

func (TrogonError) DebugInfo

func (e TrogonError) DebugInfo() *DebugInfo

func (TrogonError) Domain

func (e TrogonError) Domain() string

func (TrogonError) Error

func (e TrogonError) Error() string

func (TrogonError) Help

func (e TrogonError) Help() *Help

func (TrogonError) ID

func (e TrogonError) ID() string

func (TrogonError) Is

func (e TrogonError) Is(target error) bool

func (TrogonError) LocalizedMessage

func (e TrogonError) LocalizedMessage() *LocalizedMessage

func (TrogonError) Message

func (e TrogonError) Message() string

func (TrogonError) Metadata

func (e TrogonError) Metadata() Metadata

func (TrogonError) Reason

func (e TrogonError) Reason() string

func (TrogonError) RetryInfo

func (e TrogonError) RetryInfo() *RetryInfo

func (TrogonError) SourceID

func (e TrogonError) SourceID() string

func (TrogonError) SpecVersion

func (e TrogonError) SpecVersion() int

func (TrogonError) Subject

func (e TrogonError) Subject() string

func (TrogonError) Time

func (e TrogonError) Time() *time.Time

func (TrogonError) Unwrap

func (e TrogonError) Unwrap() error

func (TrogonError) Visibility

func (e TrogonError) Visibility() Visibility

func (*TrogonError) WithChanges

func (e *TrogonError) WithChanges(changes ...ChangeOption) *TrogonError

WithChanges applies multiple changes in a single copy operation for efficiency

type Visibility

type Visibility int

Visibility controls information disclosure across trust boundaries

const (
	VisibilityInternal Visibility = 0
	VisibilityPrivate  Visibility = 1
	VisibilityPublic   Visibility = 2
)

func (Visibility) String

func (v Visibility) String() string

Jump to

Keyboard shortcuts

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