Validation & Error Handling

6 minute read

Module Overview

Validation and error handling are critical for creating reliable, user-friendly Terraform providers. This module covers comprehensive strategies for validating inputs, handling errors gracefully, and providing clear diagnostic messages to users.

Why Validation Matters

Proper validation provides several key benefits:

1. Early Error Detection

Catch configuration errors during plan phase rather than apply:

# Invalid configuration caught during plan
resource "cachecloud_cache" "example" {
  name      = "my-cache"
  engine    = "invalid"  # Error: engine must be 'redis' or 'memcached'
  node_type = "cache.t3.micro"
}

2. Clear Error Messages

Guide users to correct their configuration:

Error: Invalid Cache Engine

  with cachecloud_cache.example,
  on main.tf line 3, in resource "cachecloud_cache" "example":
   3:   engine = "invalid"

Engine must be either 'redis' or 'memcached', got: invalid

3. Prevent Invalid API Calls

Validate before making expensive API calls:

// Validate input before API call
if cache.NumNodes < 1 || cache.NumNodes > 10 {
    resp.Diagnostics.AddError(
        "Invalid Node Count",
        fmt.Sprintf("num_nodes must be between 1 and 10, got: %d", cache.NumNodes),
    )
    return
}

// Only make API call with valid input
result, err := r.client.CreateCache(ctx, input)

Validation Levels

Terraform Plugin Framework provides multiple validation layers:

Schema-Level Validation

Built-in and custom validators attached to attributes:

Schema: schema.Schema{
    Attributes: map[string]schema.Attribute{
        "engine": schema.StringAttribute{
            Required: true,
            Validators: []validator.String{
                stringvalidator.OneOf("redis", "memcached"),
            },
        },
        "port": schema.Int64Attribute{
            Optional: true,
            Validators: []validator.Int64{
                int64validator.Between(1024, 65535),
            },
        },
    },
}

Plan-Time Validation

Complex validation requiring multiple attributes:

func (r *CacheResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
    var data CacheModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    
    // Validate engine-specific constraints
    if data.Engine.ValueString() == "memcached" && data.NumNodes.ValueInt64() > 20 {
        resp.Diagnostics.AddError(
            "Invalid Configuration",
            "Memcached clusters support maximum 20 nodes",
        )
    }
}

API-Level Validation

Server-side validation from API responses:

result, err := r.client.CreateCache(ctx, input)
if err != nil {
    if client.IsValidationError(err) {
        resp.Diagnostics.AddError(
            "Configuration Rejected by API",
            fmt.Sprintf("The cache service rejected the configuration: %s", err.Error()),
        )
    }
    return
}

Error Handling Strategies

Error Types

Classify errors for appropriate handling:

// User configuration errors
resp.Diagnostics.AddAttributeError(
    path.Root("engine"),
    "Invalid Engine",
    "Engine must be 'redis' or 'memcached'",
)

// API errors
resp.Diagnostics.AddError(
    "API Request Failed",
    fmt.Sprintf("Failed to create cache: %s", err.Error()),
)

// Warnings (non-blocking)
resp.Diagnostics.AddWarning(
    "Deprecated Configuration",
    "The 'legacy_mode' attribute is deprecated and will be removed in v2.0",
)

Error Context

Provide helpful context for troubleshooting:

resp.Diagnostics.AddError(
    "Cache Creation Failed",
    fmt.Sprintf(
        "Unable to create cache '%s' with node type '%s'. "+
        "Verify the node type is supported in your region. "+
        "API Error: %s",
        data.Name.ValueString(),
        data.NodeType.ValueString(),
        err.Error(),
    ),
)

Diagnostic Messages

Terraform uses diagnostics to communicate with users:

Error Severity Levels

// Error: Blocks operation, must be fixed
resp.Diagnostics.AddError("title", "details")

// Warning: Allows operation, should be addressed
resp.Diagnostics.AddWarning("title", "details")

// Attribute Error: Points to specific attribute
resp.Diagnostics.AddAttributeError(
    path.Root("attribute_name"),
    "title",
    "details",
)

Message Quality

Good error messages include:

  1. What went wrong: Clear statement of the problem
  2. Why it’s wrong: Explanation of the constraint
  3. How to fix it: Guidance for resolution
// ❌ Bad
resp.Diagnostics.AddError("Error", "Invalid value")

// ✅ Good
resp.Diagnostics.AddAttributeError(
    path.Root("port"),
    "Invalid Port Number",
    fmt.Sprintf(
        "Port must be between 1024 and 65535, got: %d. "+
        "Use a port number in the valid range or omit to use default (6379).",
        port,
    ),
)

Module Chapters

This module covers validation and error handling in depth:

Chapter 1: Schema-Level Validation →

Learn to use built-in validators and create custom validators for attribute-level validation.

Topics:

  • Built-in validators (OneOf, Between, RegexMatches)
  • Custom validator implementation
  • Combining multiple validators
  • Conditional validation
  • Validator testing

Chapter 2: Plan-Time Validation →

Implement complex validation requiring multiple attributes or external data.

Topics:

  • ValidateConfig interface
  • Cross-attribute validation
  • Validating attribute combinations
  • Conditional requirements
  • Plan modifiers with validation

Chapter 3: Error Types & Wrapping →

Design error type hierarchies and wrap errors with context.

Topics:

  • Custom error types
  • Error wrapping patterns
  • Error classification
  • Sentinel errors
  • Error inspection

Chapter 4: Diagnostic Messages →

Write clear, actionable diagnostic messages for users.

Topics:

  • Error vs warning diagnostics
  • Attribute-specific errors
  • Error message formatting
  • Providing resolution guidance
  • Detail vs summary

Chapter 5: Validation Patterns →

Common validation scenarios and reusable patterns.

Topics:

  • String validation (email, URL, regex)
  • Number ranges and constraints
  • List and map validation
  • Conditional validation
  • Multi-attribute validation

Chapter 6: API Error Handling →

Handle API errors gracefully and translate them for users.

Topics:

  • Parsing API errors
  • Rate limiting and retries
  • Transient vs permanent errors
  • Error recovery strategies
  • Timeout handling

Chapter 7: Validation Lab →

Build comprehensive validation for a resource with hands-on exercises.

Topics:

  • Implementing custom validators
  • Multi-level validation
  • Error message design
  • Testing validation logic
  • Complete validation suite

Chapter 8: Validation Practice →

Practice validation skills with exercises and challenges.

Topics:

  • Validation exercises
  • Error handling challenges
  • Diagnostic message improvement
  • Real-world scenarios
  • Practice solutions

Best Practices

1. Validate Early

// ✅ Good: Validate at schema level
"engine": schema.StringAttribute{
    Required: true,
    Validators: []validator.String{
        stringvalidator.OneOf("redis", "memcached"),
    },
}

// ❌ Bad: Validate during API call
result, err := r.client.CreateCache(ctx, input)
if err != nil && strings.Contains(err.Error(), "invalid engine") {
    // Too late - already made API call
}

2. Provide Clear Messages

// ✅ Good: Specific, actionable message
resp.Diagnostics.AddError(
    "Invalid Node Type",
    fmt.Sprintf(
        "Node type '%s' is not supported. "+
        "Supported types: cache.t3.micro, cache.t3.small, cache.t3.medium. "+
        "Check documentation for complete list: https://docs.example.com/node-types",
        nodeType,
    ),
)

// ❌ Bad: Vague message
resp.Diagnostics.AddError("Error", "Invalid node type")

3. Use Appropriate Severity

// ✅ Good: Error for blocking issues
if cache.Engine == "" {
    resp.Diagnostics.AddError("Required", "Engine is required")
}

// ✅ Good: Warning for deprecation
if cache.LegacyMode {
    resp.Diagnostics.AddWarning(
        "Deprecated Attribute",
        "legacy_mode is deprecated and will be removed in v2.0",
    )
}

// ❌ Bad: Error for non-blocking issue
if cache.LegacyMode {
    resp.Diagnostics.AddError("Deprecated", "Don't use legacy_mode")
}

4. Point to Specific Attributes

// ✅ Good: Attribute-specific error
resp.Diagnostics.AddAttributeError(
    path.Root("num_nodes"),
    "Invalid Node Count",
    "num_nodes must be between 1 and 10",
)

// ❌ Bad: Generic error
resp.Diagnostics.AddError(
    "Invalid Configuration",
    "num_nodes must be between 1 and 10",
)

Validation Checklist

When implementing validation:

  • ✅ Validate at the earliest possible stage
  • ✅ Use schema validators for simple rules
  • ✅ Implement ValidateConfig for complex rules
  • ✅ Provide clear, actionable error messages
  • ✅ Include context and resolution guidance
  • ✅ Point errors to specific attributes
  • ✅ Use appropriate diagnostic severity
  • ✅ Test validation with invalid inputs
  • ✅ Document validation rules
  • ✅ Handle API errors gracefully

What You’ll Learn

By the end of this module, you’ll be able to:

  • ✅ Implement validators at multiple levels
  • ✅ Create custom validators for domain-specific rules
  • ✅ Write clear, user-friendly error messages
  • ✅ Handle API errors gracefully
  • ✅ Classify and wrap errors appropriately
  • ✅ Validate complex attribute combinations
  • ✅ Test validation logic comprehensively
  • ✅ Follow validation best practices

Quick Reference

Common Validators

import (
    "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
    "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
    "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
)

// String validators
stringvalidator.OneOf("value1", "value2")
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z]+$`), "message")
stringvalidator.LengthBetween(1, 100)

// Number validators
int64validator.Between(1, 100)
int64validator.AtLeast(1)
int64validator.AtMost(100)

// List validators
listvalidator.SizeAtLeast(1)
listvalidator.SizeAtMost(10)
listvalidator.SizeBetween(1, 10)

Error Handling Template

func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // 1. Get configuration
    var data Model
    resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // 2. Validate (if needed beyond schema)
    if err := r.validate(&data); err != nil {
        resp.Diagnostics.AddError("Validation Failed", err.Error())
        return
    }
    
    // 3. Call API
    result, err := r.client.Create(ctx, input)
    if err != nil {
        resp.Diagnostics.AddError(
            "API Call Failed",
            fmt.Sprintf("Unable to create resource: %s", err.Error()),
        )
        return
    }
    
    // 4. Save state
    resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

Getting Started

Begin with Chapter 1: Schema-Level Validation to learn how to implement attribute-level validation using built-in and custom validators.

Start Chapter 1 →


Previous: ← Testing Practice
Next: Schema-Level Validation →

Last updated: June 30, 2026

Updated: