Skip to content

Error Formatting

Error formatting in ewrap provides flexible ways to present errors in different formats and contexts. This capability is crucial for logging, debugging, API responses, and system integration. Let's explore how to effectively format errors to meet various needs in your application.

Understanding Error Formatting

When an error occurs in your system, you might need to present it in different ways depending on the context:

  • As JSON for API responses
  • As YAML for configuration-related errors
  • As structured text for logging
  • As user-friendly messages for end users

ewrap provides formatting options to handle all these cases while maintaining the rich context and metadata associated with your errors.

JSON Formatting

JSON formatting is particularly useful for API responses and structured logging. Here's how to work with JSON formatting in ewrap:

func handleAPIError(w http.ResponseWriter, err error) {
    if wrappedErr, ok := err.(*ewrap.Error); ok {
        // Convert error to JSON with full context
        jsonOutput, err := wrappedErr.ToJSON(
            ewrap.WithTimestampFormat(time.RFC3339),
            ewrap.WithStackTrace(true),
        )

        if err != nil {
            // Handle formatting error
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(jsonOutput))
    }
}

The resulting JSON might look like this:

{
    "message": "failed to process user order",
    "timestamp": "2024-03-15T14:30:00Z",
    "type": "database",
    "severity": "error",
    "stack": "main.processOrder:/app/main.go:42\nmain.handleRequest:/app/main.go:28",
    "metadata": {
        "user_id": "12345",
        "order_id": "ORD-789",
        "attempt": 1
    },
    "cause": {
        "message": "database connection timeout",
        "type": "database",
        "severity": "critical"
    }
}

YAML Formatting

YAML formatting can be particularly useful for configuration-related errors or when you need a more human-readable format:

func logConfigurationError(err error, logger Logger) {
    if wrappedErr, ok := err.(*ewrap.Error); ok {
        // Convert error to YAML for logging
        yamlOutput, err := wrappedErr.ToYAML(
            ewrap.WithStackTrace(true),
        )

        if err != nil {
            logger.Error("failed to format error", "error", err)
            return
        }

        logger.Error("configuration error occurred", "details", yamlOutput)
    }
}

The formatted YAML might look like this:

message: failed to load configuration
timestamp: 2024-03-15T14:30:00Z
type: configuration
severity: critical
stack: |
    main.loadConfig:/app/config.go:25
    main.initialize:/app/main.go:15
metadata:
    config_file: /etc/myapp/config.yaml
    invalid_fields:
        - database.host
        - database.port
cause:
    message: invalid port number
    type: validation

Custom Formatting

Sometimes you need to create custom formats for specific use cases. Here's how to build custom formatters:

type ErrorFormatter struct {
    TimestampFormat string
    IncludeStack    bool
    IncludeMetadata bool
    MaxStackDepth   int
}

func NewErrorFormatter() *ErrorFormatter {
    return &ErrorFormatter{
        TimestampFormat: time.RFC3339,
        IncludeStack:    true,
        IncludeMetadata: true,
        MaxStackDepth:   10,
    }
}

func (f *ErrorFormatter) Format(err *ewrap.Error) map[string]interface{} {
    // Create base error information
    formatted := map[string]interface{}{
        "message":   err.Error(),
        "timestamp": time.Now().Format(f.TimestampFormat),
    }

    // Add stack trace if enabled
    if f.IncludeStack {
        formatted["stack"] = f.formatStack(err.Stack())
    }

    // Add metadata if enabled
    if f.IncludeMetadata {
        metadata := make(map[string]interface{})
        // Extract and format metadata...
        formatted["metadata"] = metadata
    }

    return formatted
}

func (f *ErrorFormatter) formatStack(stack string) []string {
    lines := strings.Split(stack, "\n")
    if len(lines) > f.MaxStackDepth {
        lines = lines[:f.MaxStackDepth]
    }
    return lines
}

User-Friendly Error Messages

When presenting errors to end users, you often need to transform technical errors into user-friendly messages while preserving the technical details for logging:

type UserErrorFormatter struct {
    translations map[ErrorType]string
    logger       Logger
}

func NewUserErrorFormatter(logger Logger) *UserErrorFormatter {
    return &UserErrorFormatter{
        translations: map[ErrorType]string{
            ErrorTypeValidation:    "The provided information is invalid",
            ErrorTypeNotFound:      "The requested resource could not be found",
            ErrorTypePermission:    "You don't have permission to perform this action",
            ErrorTypeDatabase:      "A system error occurred",
            ErrorTypeNetwork:       "Connection issues detected",
            ErrorTypeConfiguration: "System configuration error",
        },
        logger: logger,
    }
}

func (f *UserErrorFormatter) FormatForUser(err error) string {
    // Always log the full technical error
    if wrappedErr, ok := err.(*ewrap.Error); ok {
        f.logger.Error("error occurred",
            "technical_details", wrappedErr.ToJSON())

        // Get error context
        ctx := getErrorContext(wrappedErr)

        // Return translated message
        if msg, ok := f.translations[ctx.Type]; ok {
            return msg
        }
    }

    // Default message for unknown errors
    return "An unexpected error occurred"
}

Best Practices for Error Formatting

1. Security-Conscious Formatting

Be careful about what information you expose in different contexts:

func formatErrorResponse(err error, internal bool) interface{} {
    wrappedErr, ok := err.(*ewrap.Error)
    if !ok {
        return map[string]string{"message": "Internal Server Error"}
    }

    if internal {
        // Full details for internal logging
        return map[string]interface{}{
            "message":   wrappedErr.Error(),
            "stack":     wrappedErr.Stack(),
            "metadata":  wrappedErr.GetAllMetadata(),
            "type":      getErrorContext(wrappedErr).Type,
            "severity": getErrorContext(wrappedErr).Severity,
        }
    }

    // Limited information for external responses
    return map[string]string{
        "message": sanitizeErrorMessage(wrappedErr.Error()),
        "code":    getPublicErrorCode(wrappedErr),
    }
}

2. Consistent Format Structure

Maintain consistent error format structures across your application:

type StandardErrorResponse struct {
    Message   string                 `json:"message"`
    Code      string                 `json:"code"`
    Details   map[string]interface{} `json:"details,omitempty"`
    RequestID string                 `json:"request_id,omitempty"`
    Timestamp string                 `json:"timestamp"`
}

func NewStandardErrorResponse(err error, requestID string) StandardErrorResponse {
    return StandardErrorResponse{
        Message:   getErrorMessage(err),
        Code:      getErrorCode(err),
        Details:   getErrorDetails(err),
        RequestID: requestID,
        Timestamp: time.Now().UTC().Format(time.RFC3339),
    }
}

3. Context-Aware Formatting

Adjust formatting based on the execution context:

func formatErrorByEnvironment(err error, env string) interface{} {
    switch env {
    case "development":
        // Include everything in development
        return formatWithFullDetails(err)
    case "testing":
        // Include stack traces but sanitize sensitive data
        return formatForTesting(err)
    case "production":
        // Minimal public information
        return formatForProduction(err)
    default:
        return formatWithDefaultSettings(err)
    }
}