Common Issues

7 minute read

Common Issues Overview

Understanding typical provider problems and their solutions accelerates development and improves reliability.

State Management Issues

Issue: State Drift

Symptoms:

terraform plan
# Shows changes even though nothing changed in configuration

Causes:

  • Manual changes outside Terraform
  • Read() not refreshing state correctly
  • Computed attributes changing unexpectedly

Solution:

func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state CacheResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    // Fetch current state from API
    cache, err := r.client.GetCache(ctx, state.ID.ValueString())
    if err != nil {
        if isNotFoundError(err) {
            // Resource deleted outside Terraform
            tflog.Warn(ctx, "Resource not found, removing from state")
            resp.State.RemoveResource(ctx)
            return
        }
        resp.Diagnostics.AddError("Read Error", err.Error())
        return
    }
    
    // Update state with current values
    state.Name = types.StringValue(cache.Name)
    state.NodeType = types.StringValue(cache.NodeType)
    
    // Only update computed fields if they changed
    if cache.Status != state.Status.ValueString() {
        state.Status = types.StringValue(cache.Status)
    }
    
    resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

Issue: Persistent Diff

Symptoms:

terraform apply
# Successfully applies
terraform plan
# Still shows changes

Causes:

  • API returns data in different format
  • Normalization not applied consistently
  • Computed values not stable

Solution:

// Normalize before comparison
schema.StringAttribute{
    Description: "CIDR block",
    Required:    true,
    PlanModifiers: []planmodifier.String{
        stringplanmodifier.UseStateForUnknown(),
        // Custom normalizer
        &cidrNormalizer{},
    },
}

type cidrNormalizer struct{}

func (m cidrNormalizer) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
    if req.PlanValue.IsUnknown() || req.PlanValue.IsNull() {
        return
    }
    
    // Normalize CIDR format
    _, ipnet, err := net.ParseCIDR(req.PlanValue.ValueString())
    if err != nil {
        return
    }
    
    // Use normalized format
    resp.PlanValue = types.StringValue(ipnet.String())
}

Issue: State Migration Failure

Symptoms:

Error: Failed to decode state: ...

Causes:

  • Missing state upgrader
  • Incorrect version number
  • Type mismatch in migration

Solution:

func (r *CacheResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
    return map[int64]resource.StateUpgrader{
        0: {
            PriorSchema: &schema.Schema{
                Attributes: map[string]schema.Attribute{
                    "instance_type": schema.StringAttribute{},
                },
            },
            StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
                // Safely extract old state
                var raw map[string]interface{}
                if err := json.Unmarshal(req.RawState.JSON, &raw); err != nil {
                    resp.Diagnostics.AddError("Migration Error", 
                        fmt.Sprintf("Failed to parse state: %s", err))
                    return
                }
                
                // Migrate fields
                newState := CacheResourceModel{
                    NodeType: types.StringValue(raw["instance_type"].(string)),
                }
                
                resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
            },
        },
    }
}

Type System Issues

Issue: Cannot Unmarshal

Symptoms:

Error: cannot unmarshal string into Go value of type []string

Causes:

  • Struct tags incorrect
  • Type mismatch between schema and model
  • Missing ElementsAs conversion

Solution:

// ❌ Bad: Direct assignment
var subnets []string
subnets = plan.SubnetIDs  // Won't compile

// ✅ Good: Proper conversion
var subnets []string
resp.Diagnostics.Append(plan.SubnetIDs.ElementsAs(ctx, &subnets, false)...)
if resp.Diagnostics.HasError() {
    return
}

Issue: Null vs Unknown

Symptoms:

panic: value is unknown

Causes:

  • Not checking for null/unknown before accessing
  • Incorrect plan modifier

Solution:

// ✅ Good: Check before access
if !plan.Name.IsNull() && !plan.Name.IsUnknown() {
    name := plan.Name.ValueString()
    // Use name safely
}

// ✅ Good: Handle in plan modifier
type defaultModifier struct {
    default string
}

func (m defaultModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
    if req.ConfigValue.IsNull() {
        resp.PlanValue = types.StringValue(m.default)
        return
    }
    
    if req.ConfigValue.IsUnknown() {
        resp.PlanValue = req.ConfigValue
        return
    }
    
    resp.PlanValue = req.ConfigValue
}

Issue: Type Conversion Panic

Symptoms:

panic: interface conversion: interface {} is nil, not string

Causes:

  • Unsafe type assertion
  • Missing nil checks

Solution:

// ❌ Bad: Unsafe assertion
value := raw["field"].(string)

// ✅ Good: Safe assertion
if rawValue, ok := raw["field"]; ok {
    if strValue, ok := rawValue.(string); ok {
        // Use strValue safely
    }
}

// ✅ Better: Type switch
switch v := raw["field"].(type) {
case string:
    // Handle string
case float64:
    // Handle number
case nil:
    // Handle nil
default:
    // Handle unknown type
}

API Integration Issues

Issue: Rate Limiting

Symptoms:

Error: 429 Too Many Requests

Causes:

  • Too many API calls
  • No retry logic
  • Missing rate limiting

Solution:

import "golang.org/x/time/rate"

type RateLimitedClient struct {
    client  *Client
    limiter *rate.Limiter
}

func NewRateLimitedClient(client *Client, rps float64) *RateLimitedClient {
    return &RateLimitedClient{
        client:  client,
        limiter: rate.NewLimiter(rate.Limit(rps), 10),
    }
}

func (c *RateLimitedClient) GetCache(ctx context.Context, id string) (*Cache, error) {
    // Wait for rate limiter
    if err := c.limiter.Wait(ctx); err != nil {
        return nil, err
    }
    
    return c.client.GetCache(ctx, id)
}

Issue: Timeout Errors

Symptoms:

Error: context deadline exceeded

Causes:

  • Long-running operations
  • Network issues
  • No retry logic

Solution:

func (c *Client) CreateCacheWithRetry(ctx context.Context, input *CreateCacheInput) (*Cache, error) {
    var cache *Cache
    var err error
    
    // Retry with exponential backoff
    backoff := time.Second
    maxRetries := 3
    
    for attempt := 0; attempt < maxRetries; attempt++ {
        cache, err = c.CreateCache(ctx, input)
        
        if err == nil {
            return cache, nil
        }
        
        // Check if retryable
        if !isRetryable(err) {
            return nil, err
        }
        
        // Check context
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(backoff):
            backoff *= 2
        }
    }
    
    return nil, fmt.Errorf("max retries exceeded: %w", err)
}

func isRetryable(err error) bool {
    if apiErr, ok := err.(*APIError); ok {
        return apiErr.StatusCode >= 500 || apiErr.StatusCode == 429
    }
    return false
}

Issue: Authentication Errors

Symptoms:

Error: 401 Unauthorized
Error: 403 Forbidden

Causes:

  • Invalid API key
  • Expired credentials
  • Missing permissions

Solution:

func (p *provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config providerModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    
    // Validate API key format
    apiKey := config.APIKey.ValueString()
    if !isValidAPIKeyFormat(apiKey) {
        resp.Diagnostics.AddError(
            "Invalid API Key",
            "API key must be in format: ck_xxxx",
        )
        return
    }
    
    // Test authentication
    client := NewClient(apiKey)
    if err := client.TestAuth(ctx); err != nil {
        resp.Diagnostics.AddError(
            "Authentication Failed",
            fmt.Sprintf("Failed to authenticate with provided API key: %s", err),
        )
        return
    }
    
    resp.ResourceData = client
}

Performance Issues

Issue: Slow Operations

Symptoms:

# Terraform operations take minutes

Causes:

  • Individual API calls in loops
  • No caching
  • Inefficient data structures

Solution:

// ❌ Bad: Sequential calls
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var caches []*Cache
    for _, id := range cacheIDs {
        cache, err := d.client.GetCache(ctx, id)
        // Process...
    }
}

// ✅ Good: Batch operation
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    // Single batch call
    caches, err := d.client.BatchGetCaches(ctx, cacheIDs)
    if err != nil {
        resp.Diagnostics.AddError("API Error", err.Error())
        return
    }
}

Issue: Memory Leaks

Symptoms:

# Provider memory usage grows unbounded

Causes:

  • Unclosed connections
  • Unbounded caches
  • Goroutine leaks

Solution:

// ✅ Good: Bounded cache with cleanup
type BoundedCache struct {
    cache    *lru.Cache
    mu       sync.RWMutex
    stopChan chan struct{}
}

func NewBoundedCache(size int, ttl time.Duration) *BoundedCache {
    cache, _ := lru.New(size)
    bc := &BoundedCache{
        cache:    cache,
        stopChan: make(chan struct{}),
    }
    
    // Cleanup goroutine
    go bc.cleanup(ttl)
    
    return bc
}

func (bc *BoundedCache) cleanup(ttl time.Duration) {
    ticker := time.NewTicker(ttl)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            bc.mu.Lock()
            bc.cache.Purge()
            bc.mu.Unlock()
        case <-bc.stopChan:
            return
        }
    }
}

func (bc *BoundedCache) Close() {
    close(bc.stopChan)
}

Issue: Goroutine Explosion

Symptoms:

# Thousands of goroutines created

Causes:

  • Unbounded parallelism
  • Not waiting for goroutines
  • Missing context cancellation

Solution:

// ✅ Good: Controlled concurrency
func (c *Client) ParallelFetch(ctx context.Context, ids []string) ([]*Cache, error) {
    semaphore := make(chan struct{}, 10) // Max 10 concurrent
    results := make(chan *Cache, len(ids))
    errors := make(chan error, len(ids))
    
    var wg sync.WaitGroup
    for _, id := range ids {
        wg.Add(1)
        go func(cacheID string) {
            defer wg.Done()
            
            select {
            case semaphore <- struct{}{}:
                defer func() { <-semaphore }()
            case <-ctx.Done():
                errors <- ctx.Err()
                return
            }
            
            cache, err := c.GetCache(ctx, cacheID)
            if err != nil {
                errors <- err
                return
            }
            results <- cache
        }(id)
    }
    
    // Wait with timeout
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()
    
    select {
    case <-done:
        close(results)
        close(errors)
    case <-ctx.Done():
        return nil, ctx.Err()
    }
    
    // Collect results
    var caches []*Cache
    for cache := range results {
        caches = append(caches, cache)
    }
    
    // Check for errors
    var errs []error
    for err := range errors {
        errs = append(errs, err)
    }
    if len(errs) > 0 {
        return nil, errs[0]
    }
    
    return caches, nil
}

Validation Issues

Issue: Silent Validation Failures

Symptoms:

# Invalid configuration accepted

Causes:

  • Missing validators
  • Validator not returning errors

Solution:

// ✅ Good: Proper validation
schema.StringAttribute{
    Required: true,
    Validators: []validator.String{
        stringvalidator.LengthBetween(1, 255),
        stringvalidator.RegexMatches(
            regexp.MustCompile(`^[a-zA-Z0-9-]+$`),
            "must contain only alphanumeric characters and hyphens",
        ),
    },
}

// Custom validator with proper error handling
type customValidator struct{}

func (v customValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
    if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
        return
    }
    
    value := req.ConfigValue.ValueString()
    
    if !isValid(value) {
        resp.Diagnostics.AddAttributeError(
            req.Path,
            "Validation Error",
            fmt.Sprintf("Value '%s' is invalid: must meet criteria X", value),
        )
    }
}

Testing Issues

Issue: Flaky Tests

Symptoms:

# Tests pass/fail inconsistently

Causes:

  • Race conditions
  • External dependencies
  • Time-dependent logic

Solution:

// ✅ Good: Deterministic test
func TestCacheResource_Create(t *testing.T) {
    // Use fixed time
    fixedTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
    
    // Use mock client
    mockClient := &MockClient{
        CreateCacheFunc: func(ctx context.Context, input *CreateCacheInput) (*Cache, error) {
            return &Cache{
                ID:        "test-id",
                Name:      input.Name,
                CreatedAt: fixedTime,
            }, nil
        },
    }
    
    // Test with deterministic behavior
    r := NewCacheResource()
    r.client = mockClient
    
    // Run test...
}

Key Takeaways

  • ✅ Understand common state issues
  • ✅ Handle type conversions safely
  • ✅ Implement retry logic for API calls
  • ✅ Optimize for performance
  • ✅ Validate inputs thoroughly
  • ✅ Write deterministic tests
  • ✅ Profile memory usage
  • ✅ Control concurrency

What’s Next?

Practice debugging in real-world scenarios with hands-on exercises.

Next: Troubleshooting Lab →


Previous: ← Debugging Techniques
Next: Troubleshooting Lab →

Last updated: July 2, 2026

Updated: