Error Types & Wrapping

8 minute read

Error Types Overview

Well-designed error types improve debugging and enable appropriate error handling based on error classification.

Custom Error Types

Basic Custom Error

package client

import "fmt"

type CacheError struct {
    Operation string
    CacheID   string
    Err       error
}

func (e *CacheError) Error() string {
    return fmt.Sprintf("cache operation '%s' failed for cache '%s': %v",
        e.Operation, e.CacheID, e.Err)
}

func (e *CacheError) Unwrap() error {
    return e.Err
}

// Usage
func (c *Client) DeleteCache(ctx context.Context, id string) error {
    err := c.callAPI(ctx, "DELETE", fmt.Sprintf("/caches/%s", id), nil, nil)
    if err != nil {
        return &CacheError{
            Operation: "delete",
            CacheID:   id,
            Err:       err,
        }
    }
    return nil
}

Error with HTTP Details

type APIError struct {
    StatusCode int
    Method     string
    URL        string
    Message    string
    RequestID  string
    Err        error
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API request failed: %s %s (status: %d, request_id: %s): %s",
        e.Method, e.URL, e.StatusCode, e.RequestID, e.Message)
}

func (e *APIError) Unwrap() error {
    return e.Err
}

// Check for specific status codes
func (e *APIError) IsNotFound() bool {
    return e.StatusCode == 404
}

func (e *APIError) IsRateLimited() bool {
    return e.StatusCode == 429
}

func (e *APIError) IsServerError() bool {
    return e.StatusCode >= 500
}

Sentinel Errors

Sentinel errors are predefined error values for specific conditions:

package client

import "errors"

var (
    ErrNotFound          = errors.New("resource not found")
    ErrAlreadyExists     = errors.New("resource already exists")
    ErrInvalidInput      = errors.New("invalid input")
    ErrUnauthorized      = errors.New("unauthorized")
    ErrRateLimitExceeded = errors.New("rate limit exceeded")
    ErrServerError       = errors.New("server error")
)

// Usage
func (c *Client) GetCache(ctx context.Context, id string) (*Cache, error) {
    resp, err := c.callAPI(ctx, "GET", fmt.Sprintf("/caches/%s", id), nil, nil)
    if err != nil {
        if resp != nil && resp.StatusCode == 404 {
            return nil, ErrNotFound
        }
        return nil, err
    }
    
    var cache Cache
    if err := json.Unmarshal(resp.Body, &cache); err != nil {
        return nil, fmt.Errorf("failed to parse response: %w", err)
    }
    
    return &cache, nil
}

// Check for sentinel errors
cache, err := client.GetCache(ctx, id)
if errors.Is(err, client.ErrNotFound) {
    // Handle not found
}

Error Classification

Create Error Hierarchy

package client

// Base error type
type Error struct {
    Code      string
    Message   string
    RequestID string
    Err       error
}

func (e *Error) Error() string {
    if e.RequestID != "" {
        return fmt.Sprintf("[%s] %s (request_id: %s)", e.Code, e.Message, e.RequestID)
    }
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func (e *Error) Unwrap() error {
    return e.Err
}

// Specific error types
type NotFoundError struct {
    *Error
    ResourceType string
    ResourceID   string
}

func NewNotFoundError(resourceType, resourceID string) *NotFoundError {
    return &NotFoundError{
        Error: &Error{
            Code:    "NOT_FOUND",
            Message: fmt.Sprintf("%s '%s' not found", resourceType, resourceID),
        },
        ResourceType: resourceType,
        ResourceID:   resourceID,
    }
}

type ValidationError struct {
    *Error
    Field  string
    Value  interface{}
    Reason string
}

func NewValidationError(field string, value interface{}, reason string) *ValidationError {
    return &ValidationError{
        Error: &Error{
            Code:    "VALIDATION_ERROR",
            Message: fmt.Sprintf("validation failed for field '%s': %s", field, reason),
        },
        Field:  field,
        Value:  value,
        Reason: reason,
    }
}

type RateLimitError struct {
    *Error
    Limit     int
    Remaining int
    ResetAt   time.Time
}

func NewRateLimitError(limit, remaining int, resetAt time.Time) *RateLimitError {
    return &RateLimitError{
        Error: &Error{
            Code:    "RATE_LIMIT_EXCEEDED",
            Message: fmt.Sprintf("rate limit exceeded (limit: %d, reset: %s)", limit, resetAt.Format(time.RFC3339)),
        },
        Limit:     limit,
        Remaining: remaining,
        ResetAt:   resetAt,
    }
}

Error Wrapping

Standard Error Wrapping

import "fmt"

func (c *Client) CreateCache(ctx context.Context, input CreateCacheInput) (*Cache, error) {
    // Validate input
    if err := input.Validate(); err != nil {
        return nil, fmt.Errorf("invalid input: %w", err)
    }
    
    // Make API call
    resp, err := c.callAPI(ctx, "POST", "/caches", input, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create cache: %w", err)
    }
    
    // Parse response
    var cache Cache
    if err := json.Unmarshal(resp.Body, &cache); err != nil {
        return nil, fmt.Errorf("failed to parse response: %w", err)
    }
    
    return &cache, nil
}

Multi-Level Wrapping

// Resource layer
func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var data CacheModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
    
    cache, err := r.createCache(ctx, &data)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Create Cache",
            fmt.Sprintf("Could not create cache: %s", err.Error()),
        )
        return
    }
    
    // ... save state
}

// Business logic layer
func (r *CacheResource) createCache(ctx context.Context, data *CacheModel) (*client.Cache, error) {
    input := client.CreateCacheInput{
        Name:     data.Name.ValueString(),
        Engine:   data.Engine.ValueString(),
        NodeType: data.NodeType.ValueString(),
        NumNodes: int(data.NumNodes.ValueInt64()),
    }
    
    cache, err := r.client.CreateCache(ctx, input)
    if err != nil {
        return nil, fmt.Errorf("failed to create cache via API: %w", err)
    }
    
    return cache, nil
}

// Client layer
func (c *Client) CreateCache(ctx context.Context, input CreateCacheInput) (*Cache, error) {
    resp, err := c.callAPI(ctx, "POST", "/caches", input, nil)
    if err != nil {
        return nil, fmt.Errorf("API call failed: %w", err)
    }
    
    var cache Cache
    if err := json.Unmarshal(resp.Body, &cache); err != nil {
        return nil, fmt.Errorf("failed to unmarshal response: %w", err)
    }
    
    return &cache, nil
}

// HTTP layer
func (c *Client) callAPI(ctx context.Context, method, path string, body, result interface{}) (*http.Response, error) {
    req, err := c.newRequest(ctx, method, path, body)
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %w", err)
    }
    
    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("HTTP request failed: %w", err)
    }
    
    return resp, nil
}

Error Inspection

Checking Error Types

import "errors"

func handleError(err error) {
    // Check for specific error type
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        if apiErr.IsNotFound() {
            log.Println("Resource not found")
        } else if apiErr.IsRateLimited() {
            log.Println("Rate limited, retry after:", apiErr.RetryAfter)
        }
        return
    }
    
    // Check for sentinel error
    if errors.Is(err, ErrNotFound) {
        log.Println("Not found")
        return
    }
    
    // Generic error
    log.Printf("Unexpected error: %v", err)
}

Error Type Assertions

func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    cache, err := r.client.CreateCache(ctx, input)
    if err != nil {
        // Check for specific error types
        var notFoundErr *client.NotFoundError
        if errors.As(err, &notFoundErr) {
            resp.Diagnostics.AddError(
                "Resource Not Found",
                fmt.Sprintf("The %s '%s' does not exist", 
                    notFoundErr.ResourceType, 
                    notFoundErr.ResourceID),
            )
            return
        }
        
        var validationErr *client.ValidationError
        if errors.As(err, &validationErr) {
            resp.Diagnostics.AddAttributeError(
                path.Root(validationErr.Field),
                "Validation Failed",
                validationErr.Reason,
            )
            return
        }
        
        var rateLimitErr *client.RateLimitError
        if errors.As(err, &rateLimitErr) {
            resp.Diagnostics.AddError(
                "Rate Limit Exceeded",
                fmt.Sprintf(
                    "Rate limit exceeded. Limit: %d, Remaining: %d, Reset at: %s. "+
                    "Please wait before retrying.",
                    rateLimitErr.Limit,
                    rateLimitErr.Remaining,
                    rateLimitErr.ResetAt.Format(time.RFC3339),
                ),
            )
            return
        }
        
        // Generic error
        resp.Diagnostics.AddError(
            "API Error",
            fmt.Sprintf("Failed to create cache: %s", err.Error()),
        )
        return
    }
}

Helper Functions

Error Classification Helpers

package client

func IsNotFoundError(err error) bool {
    if err == nil {
        return false
    }
    
    var notFoundErr *NotFoundError
    if errors.As(err, &notFoundErr) {
        return true
    }
    
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        return apiErr.IsNotFound()
    }
    
    return errors.Is(err, ErrNotFound)
}

func IsValidationError(err error) bool {
    if err == nil {
        return false
    }
    
    var validationErr *ValidationError
    return errors.As(err, &validationErr)
}

func IsRateLimitError(err error) bool {
    if err == nil {
        return false
    }
    
    var rateLimitErr *RateLimitError
    if errors.As(err, &rateLimitErr) {
        return true
    }
    
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        return apiErr.IsRateLimited()
    }
    
    return errors.Is(err, ErrRateLimitExceeded)
}

func IsRetryableError(err error) bool {
    if err == nil {
        return false
    }
    
    // Rate limit errors are retryable
    if IsRateLimitError(err) {
        return true
    }
    
    // Server errors (5xx) are retryable
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        return apiErr.IsServerError()
    }
    
    // Network errors are retryable
    if errors.Is(err, context.DeadlineExceeded) {
        return true
    }
    
    return false
}

Error Context

Add Context to Errors

type ErrorContext struct {
    Operation string
    Resource  string
    Details   map[string]interface{}
    Err       error
}

func (e *ErrorContext) Error() string {
    detailsStr := ""
    if len(e.Details) > 0 {
        parts := make([]string, 0, len(e.Details))
        for k, v := range e.Details {
            parts = append(parts, fmt.Sprintf("%s=%v", k, v))
        }
        detailsStr = " (" + strings.Join(parts, ", ") + ")"
    }
    
    return fmt.Sprintf("%s failed for %s%s: %v",
        e.Operation, e.Resource, detailsStr, e.Err)
}

func (e *ErrorContext) Unwrap() error {
    return e.Err
}

// Usage
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan, state CacheModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    err := r.updateCache(ctx, &plan, &state)
    if err != nil {
        resp.Diagnostics.AddError(
            "Update Failed",
            err.Error(),
        )
        return
    }
}

func (r *CacheResource) updateCache(ctx context.Context, plan, state *CacheModel) error {
    cacheID := state.ID.ValueString()
    
    input := client.UpdateCacheInput{
        NumNodes: int(plan.NumNodes.ValueInt64()),
    }
    
    _, err := r.client.UpdateCache(ctx, cacheID, input)
    if err != nil {
        return &ErrorContext{
            Operation: "update",
            Resource:  "cache",
            Details: map[string]interface{}{
                "cache_id":  cacheID,
                "num_nodes": input.NumNodes,
            },
            Err: err,
        }
    }
    
    return nil
}

Best Practices

1. Use Descriptive Error Types

// ✅ Good: Specific error type
type CacheNotFoundError struct {
    CacheID string
}

func (e *CacheNotFoundError) Error() string {
    return fmt.Sprintf("cache '%s' not found", e.CacheID)
}

// ❌ Bad: Generic error
return errors.New("not found")

2. Wrap Errors with Context

// ✅ Good: Add context at each layer
func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    cache, err := r.client.CreateCache(ctx, input)
    if err != nil {
        resp.Diagnostics.AddError(
            "Cache Creation Failed",
            fmt.Sprintf("Unable to create cache '%s': %s", name, err.Error()),
        )
        return
    }
}

// ❌ Bad: No context
if err != nil {
    resp.Diagnostics.AddError("Error", err.Error())
}

3. Implement Error Helper Functions

// ✅ Good: Helper functions for common checks
if client.IsNotFoundError(err) {
    // Handle not found
}

// ❌ Bad: Repeated type assertions
var notFoundErr *NotFoundError
if errors.As(err, &notFoundErr) {
    // ...
}

4. Include Debugging Information

// ✅ Good: Include request ID for debugging
type APIError struct {
    RequestID  string
    StatusCode int
    Message    string
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API error (request_id: %s, status: %d): %s",
        e.RequestID, e.StatusCode, e.Message)
}

// ❌ Bad: Minimal information
type APIError struct {
    Message string
}

Key Takeaways

  • ✅ Create custom error types for specific scenarios
  • ✅ Use sentinel errors for well-known conditions
  • ✅ Wrap errors to add context at each layer
  • ✅ Implement Unwrap() for error unwrapping
  • ✅ Use errors.Is() and errors.As() for error inspection
  • ✅ Provide helper functions for error classification
  • ✅ Include debugging information (request IDs, timestamps)
  • ✅ Make errors actionable with clear messages
  • ✅ Classify errors (retryable, permanent, user error)
  • ✅ Test error handling paths

What’s Next?

Learn how to write clear, user-friendly diagnostic messages.

Next: Diagnostic Messages →


Previous: ← Plan-Time Validation

Last updated: June 30, 2026

Updated: