Common Issues
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.
Previous: ← Debugging Techniques
Next: Troubleshooting Lab →
Last updated: July 2, 2026