Error Types & Wrapping
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, ¬FoundErr) {
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, ¬FoundErr) {
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, ¬FoundErr) {
// ...
}
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.
Previous: ← Plan-Time Validation
Last updated: June 30, 2026