Go Custom Error Types
Custom error types in Go allow you to create domain-specific errors with additional information and behavior beyond the simple string message provided by the built-in error interface. Understanding how to create and use custom error types is crucial for building robust, maintainable applications that can handle different types of errors appropriately. This comprehensive guide will teach you everything you need to know about creating and using custom error types in Go.
Understanding Custom Error Types
What Are Custom Error Types?
Custom error types in Go are user-defined types that implement the error interface and provide additional functionality beyond basic error messages. They offer:
- Domain-specific information - Errors tailored to your application's needs
- Additional data - Structured error information beyond simple strings
- Error classification - Ability to categorize and handle different error types
- Behavioral methods - Custom methods for error handling and analysis
- Error hierarchies - Structured error relationships and inheritance
Benefits of Custom Error Types
Rich Error Information
Custom error types can carry structured data about the error context.
Error Classification
Different error types can be handled differently based on their classification.
Type Safety
Compile-time checking for error handling patterns.
Extensibility
Easy to extend and modify error behavior without breaking existing code.
Creating Custom Error Types
Basic Custom Error Type
Implementing the Error Interface
Any type that implements Error() string
satisfies the error interface.
Adding Additional Fields
Custom error types can include additional fields for context.
package main
import (
"fmt"
"time"
)
func main() {
// Basic custom error type examples
fmt.Println("Basic custom error type examples:")
// Define a custom error type
type ValidationError struct {
Field string
Value interface{}
Message string
}
// Implement the Error() method
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error in field '%s': %s (value: %v)", e.Field, e.Message, e.Value)
}
// Create and use custom error
err := ValidationError{
Field: "email",
Value: "invalid-email",
Message: "invalid email format",
}
fmt.Printf("Error: %v\n", err)
// Output: Error: validation error in field 'email': invalid email format (value: invalid-email)
// Custom error with additional methods
type DatabaseError struct {
Operation string
Table string
Code int
Message string
Timestamp time.Time
}
func (e DatabaseError) Error() string {
return fmt.Sprintf("database error: %s on table '%s' failed with code %d: %s",
e.Operation, e.Table, e.Code, e.Message)
}
func (e DatabaseError) IsRetryable() bool {
// Some database error codes are retryable
return e.Code >= 500 && e.Code < 600
}
func (e DatabaseError) GetContext() map[string]interface{} {
return map[string]interface{}{
"operation": e.Operation,
"table": e.Table,
"code": e.Code,
"timestamp": e.Timestamp,
}
}
// Create and use database error
dbErr := DatabaseError{
Operation: "INSERT",
Table: "users",
Code: 1062,
Message: "duplicate entry",
Timestamp: time.Now(),
}
fmt.Printf("Database error: %v\n", dbErr)
fmt.Printf("Is retryable: %t\n", dbErr.IsRetryable())
fmt.Printf("Context: %v\n", dbErr.GetContext())
// Output:
// Database error: database error: INSERT on table 'users' failed with code 1062: duplicate entry
// Is retryable: false
// Context: map[code:1062 operation:INSERT table:users timestamp:2023-12-07 10:00:00 +0000 UTC]
// Function that returns custom error
func validateEmail(email string) error {
if email == "" {
return ValidationError{
Field: "email",
Value: email,
Message: "email is required",
}
}
if len(email) < 5 {
return ValidationError{
Field: "email",
Value: email,
Message: "email is too short",
}
}
// Simple email validation
if !contains(email, "@") {
return ValidationError{
Field: "email",
Value: email,
Message: "email must contain @ symbol",
}
}
return nil
}
// Helper function
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// Test email validation
emails := []string{"", "ab", "[email protected]", "invalid-email"}
for _, email := range emails {
err := validateEmail(email)
if err != nil {
fmt.Printf("Email '%s': %v\n", email, err)
} else {
fmt.Printf("Email '%s': valid\n", email)
}
}
// Output:
// Email '': validation error in field 'email': email is required (value: )
// Email 'ab': validation error in field 'email': email is too short (value: ab)
// Email '[email protected]': valid
// Email 'invalid-email': validation error in field 'email': email must contain @ symbol (value: invalid-email)
}
Error Classification and Hierarchies
Error Classification
Organizing errors into categories for better handling.
Error Hierarchies
Creating structured relationships between different error types.
package main
import (
"fmt"
"time"
)
func main() {
// Error classification and hierarchies examples
fmt.Println("Error classification and hierarchies examples:")
// Define base error type
type AppError struct {
Code string
Message string
Time time.Time
}
func (e AppError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Define specific error types
type ValidationError struct {
AppError
Field string
Value interface{}
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error in field '%s': %s (value: %v)",
e.Field, e.Message, e.Value)
}
type DatabaseError struct {
AppError
Operation string
Table string
SQLCode int
}
func (e DatabaseError) Error() string {
return fmt.Sprintf("database error: %s on table '%s' failed with SQL code %d: %s",
e.Operation, e.Table, e.SQLCode, e.Message)
}
type NetworkError struct {
AppError
URL string
Status int
Timeout bool
}
func (e NetworkError) Error() string {
if e.Timeout {
return fmt.Sprintf("network timeout: %s (URL: %s)", e.Message, e.URL)
}
return fmt.Sprintf("network error: %s (URL: %s, Status: %d)", e.Message, e.URL, e.Status)
}
// Error classification functions
func IsValidationError(err error) bool {
_, ok := err.(ValidationError)
return ok
}
func IsDatabaseError(err error) bool {
_, ok := err.(DatabaseError)
return ok
}
func IsNetworkError(err error) bool {
_, ok := err.(NetworkError)
return ok
}
func IsRetryableError(err error) bool {
switch e := err.(type) {
case DatabaseError:
return e.SQLCode >= 500
case NetworkError:
return e.Status >= 500 || e.Timeout
default:
return false
}
}
// Create different types of errors
validationErr := ValidationError{
AppError: AppError{
Code: "VALIDATION_ERROR",
Message: "invalid input",
Time: time.Now(),
},
Field: "username",
Value: "",
}
databaseErr := DatabaseError{
AppError: AppError{
Code: "DATABASE_ERROR",
Message: "connection failed",
Time: time.Now(),
},
Operation: "SELECT",
Table: "users",
SQLCode: 2003,
}
networkErr := NetworkError{
AppError: AppError{
Code: "NETWORK_ERROR",
Message: "request failed",
Time: time.Now(),
},
URL: "https://api.example.com/users",
Status: 500,
Timeout: false,
}
// Test error classification
errors := []error{validationErr, databaseErr, networkErr}
for _, err := range errors {
fmt.Printf("Error: %v\n", err)
fmt.Printf(" Is validation error: %t\n", IsValidationError(err))
fmt.Printf(" Is database error: %t\n", IsDatabaseError(err))
fmt.Printf(" Is network error: %t\n", IsNetworkError(err))
fmt.Printf(" Is retryable: %t\n", IsRetryableError(err))
fmt.Println()
}
// Output:
// Error: validation error in field 'username': invalid input (value: )
// Is validation error: true
// Is database error: false
// Is network error: false
// Is retryable: false
//
// Error: database error: SELECT on table 'users' failed with SQL code 2003: connection failed
// Is validation error: false
// Is database error: true
// Is network error: false
// Is retryable: true
//
// Error: network error: request failed (URL: https://api.example.com/users, Status: 500)
// Is validation error: false
// Is database error: false
// Is network error: true
// Is retryable: true
// Error handling based on classification
func handleError(err error) {
switch {
case IsValidationError(err):
fmt.Println("Handling validation error: user input needs to be corrected")
case IsDatabaseError(err):
if IsRetryableError(err) {
fmt.Println("Handling database error: retrying operation")
} else {
fmt.Println("Handling database error: checking database configuration")
}
case IsNetworkError(err):
if IsRetryableError(err) {
fmt.Println("Handling network error: retrying request")
} else {
fmt.Println("Handling network error: checking network configuration")
}
default:
fmt.Println("Handling unknown error: logging and reporting")
}
}
// Test error handling
fmt.Println("Error handling:")
for _, err := range errors {
fmt.Printf("Processing error: %v\n", err)
handleError(err)
fmt.Println()
}
// Output:
// Error handling:
// Processing error: validation error in field 'username': invalid input (value: )
// Handling validation error: user input needs to be corrected
//
// Processing error: database error: SELECT on table 'users' failed with SQL code 2003: connection failed
// Handling database error: retrying operation
//
// Processing error: network error: request failed (URL: https://10.0.0.1/users, Status: 500)
// Handling network error: retrying request
}
Advanced Custom Error Patterns
Error with Context and Metadata
Rich Error Information
Custom errors that carry extensive context and metadata.
Error Analysis Methods
Methods for analyzing and processing error information.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// Advanced custom error patterns examples
fmt.Println("Advanced custom error patterns examples:")
// Define error with rich context
type ContextualError struct {
Code string
Message string
Context map[string]interface{}
StackTrace []string
Timestamp time.Time
Function string
File string
Line int
}
func (e ContextualError) Error() string {
return fmt.Sprintf("[%s] %s (file: %s:%d, function: %s)",
e.Code, e.Message, e.File, e.Line, e.Function)
}
func (e ContextualError) GetContext() map[string]interface{} {
return e.Context
}
func (e ContextualError) GetStackTrace() []string {
return e.StackTrace
}
func (e ContextualError) IsCode(code string) bool {
return e.Code == code
}
// Function to create contextual error
func newContextualError(code, message string, context map[string]interface{}) ContextualError {
// Get caller information
pc, file, line, ok := runtime.Caller(1)
function := "unknown"
if ok {
fn := runtime.FuncForPC(pc)
if fn != nil {
function = fn.Name()
}
}
// Get stack trace
var stackTrace []string
for i := 0; i < 10; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fn := runtime.FuncForPC(pc)
if fn != nil {
stackTrace = append(stackTrace, fmt.Sprintf("%s:%d %s", file, line, fn.Name()))
}
}
return ContextualError{
Code: code,
Message: message,
Context: context,
StackTrace: stackTrace,
Timestamp: time.Now(),
Function: function,
File: file,
Line: line,
}
}
// Function that creates contextual error
func processUser(userID string, userData map[string]interface{}) error {
if userID == "" {
return newContextualError("INVALID_USER_ID", "user ID cannot be empty",
map[string]interface{}{
"userID": userID,
"userData": userData,
"operation": "processUser",
})
}
if len(userID) < 3 {
return newContextualError("USER_ID_TOO_SHORT", "user ID is too short",
map[string]interface{}{
"userID": userID,
"userData": userData,
"operation": "processUser",
"minLength": 3,
"actualLength": len(userID),
})
}
// Simulate processing
if userID == "error" {
return newContextualError("PROCESSING_ERROR", "failed to process user",
map[string]interface{}{
"userID": userID,
"userData": userData,
"operation": "processUser",
"reason": "simulated error",
})
}
return nil
}
// Test contextual error creation
userData := map[string]interface{}{
"name": "Alice",
"email": "[email protected]",
}
// Test with empty user ID
err := processUser("", userData)
if err != nil {
if ctxErr, ok := err.(ContextualError); ok {
fmt.Printf("Contextual error: %v\n", ctxErr)
fmt.Printf("Context: %v\n", ctxErr.GetContext())
fmt.Printf("Is INVALID_USER_ID: %t\n", ctxErr.IsCode("INVALID_USER_ID"))
}
}
// Output:
// Contextual error: [INVALID_USER_ID] user ID cannot be empty (file: /path/to/file.go:123, function: main.processUser)
// Context: map[minLength:3 operation:processUser userData:map[email:[email protected] name:Alice] userID:]
// Is INVALID_USER_ID: true
// Test with short user ID
err = processUser("ab", userData)
if err != nil {
if ctxErr, ok := err.(ContextualError); ok {
fmt.Printf("Contextual error: %v\n", ctxErr)
fmt.Printf("Context: %v\n", ctxErr.GetContext())
fmt.Printf("Is USER_ID_TOO_SHORT: %t\n", ctxErr.IsCode("USER_ID_TOO_SHORT"))
}
}
// Output:
// Contextual error: [USER_ID_TOO_SHORT] user ID is too short (file: /path/to/file.go:123, function: main.processUser)
// Context: map[actualLength:2 minLength:3 operation:processUser userData:map[email:[email protected] name:Alice] userID:ab]
// Is USER_ID_TOO_SHORT: true
// Test with processing error
err = processUser("error", userData)
if err != nil {
if ctxErr, ok := err.(ContextualError); ok {
fmt.Printf("Contextual error: %v\n", ctxErr)
fmt.Printf("Context: %v\n", ctxErr.GetContext())
fmt.Printf("Is PROCESSING_ERROR: %t\n", ctxErr.IsCode("PROCESSING_ERROR"))
}
}
// Output:
// Contextual error: [PROCESSING_ERROR] failed to process user (file: /path/to/file.go:123, function: main.processUser)
// Context: map[operation:processUser reason:simulated error userData:map[email:[email protected] name:Alice] userID:error]
// Is PROCESSING_ERROR: true
}
Error with Behavior and State
Stateful Errors
Errors that can change state or provide different behavior.
Error Recovery Methods
Methods for attempting error recovery or mitigation.
package main
import (
"fmt"
"time"
)
func main() {
// Error with behavior and state examples
fmt.Println("Error with behavior and state examples:")
// Define error with behavior
type RetryableError struct {
Message string
MaxRetries int
RetryCount int
LastRetry time.Time
RetryDelay time.Duration
Recoverable bool
}
func (e *RetryableError) Error() string {
return fmt.Sprintf("retryable error: %s (attempt %d/%d)",
e.Message, e.RetryCount+1, e.MaxRetries+1)
}
func (e *RetryableError) CanRetry() bool {
return e.RetryCount < e.MaxRetries
}
func (e *RetryableError) Retry() error {
if !e.CanRetry() {
return fmt.Errorf("maximum retries exceeded: %s", e.Message)
}
e.RetryCount++
e.LastRetry = time.Now()
// Simulate retry delay
time.Sleep(e.RetryDelay)
return nil
}
func (e *RetryableError) IsRecoverable() bool {
return e.Recoverable
}
func (e *RetryableError) GetRetryInfo() map[string]interface{} {
return map[string]interface{}{
"message": e.Message,
"retryCount": e.RetryCount,
"maxRetries": e.MaxRetries,
"lastRetry": e.LastRetry,
"retryDelay": e.RetryDelay,
"recoverable": e.Recoverable,
}
}
// Function that creates retryable error
func simulateOperation() error {
// Simulate operation that might fail
return &RetryableError{
Message: "network connection failed",
MaxRetries: 3,
RetryCount: 0,
RetryDelay: 100 * time.Millisecond,
Recoverable: true,
}
}
// Function that handles retryable errors
func performOperationWithRetry() error {
err := simulateOperation()
if err == nil {
return nil
}
if retryErr, ok := err.(*RetryableError); ok {
fmt.Printf("Operation failed: %v\n", retryErr)
for retryErr.CanRetry() {
fmt.Printf("Retrying... (attempt %d/%d)\n",
retryErr.RetryCount+1, retryErr.MaxRetries+1)
if retryErr.Retry() != nil {
break
}
// Simulate retry attempt
if retryErr.RetryCount == 2 {
fmt.Println("Retry successful!")
return nil
}
}
return fmt.Errorf("operation failed after %d retries: %s",
retryErr.MaxRetries, retryErr.Message)
}
return err
}
// Test retryable error
err := performOperationWithRetry()
if err != nil {
fmt.Printf("Final error: %v\n", err)
} else {
fmt.Println("Operation completed successfully")
}
// Output:
// Operation failed: retryable error: network connection failed (attempt 1/4)
// Retrying... (attempt 1/4)
// Retrying... (attempt 2/4)
// Retry successful!
// Operation completed successfully
// Error with state management
type StatefulError struct {
Message string
State string
Transitions map[string]string
Data map[string]interface{}
}
func (e *StatefulError) Error() string {
return fmt.Sprintf("stateful error [%s]: %s", e.State, e.Message)
}
func (e *StatefulError) TransitionTo(newState string) error {
if allowed, exists := e.Transitions[newState]; exists {
if allowed == e.State {
e.State = newState
return nil
}
return fmt.Errorf("invalid transition from %s to %s", e.State, newState)
}
return fmt.Errorf("unknown state: %s", newState)
}
func (e *StatefulError) GetState() string {
return e.State
}
func (e *StatefulError) SetData(key string, value interface{}) {
if e.Data == nil {
e.Data = make(map[string]interface{})
}
e.Data[key] = value
}
func (e *StatefulError) GetData(key string) interface{} {
if e.Data == nil {
return nil
}
return e.Data[key]
}
// Create stateful error
statefulErr := &StatefulError{
Message: "processing error",
State: "pending",
Transitions: map[string]string{
"processing": "pending",
"completed": "processing",
"failed": "processing",
},
Data: make(map[string]interface{}),
}
// Test state transitions
fmt.Printf("Initial state: %s\n", statefulErr.GetState())
err = statefulErr.TransitionTo("processing")
if err != nil {
fmt.Printf("Transition error: %v\n", err)
} else {
fmt.Printf("State after transition: %s\n", statefulErr.GetState())
}
// Output:
// Initial state: pending
// State after transition: processing
// Set data
statefulErr.SetData("attempts", 3)
statefulErr.SetData("lastError", "timeout")
fmt.Printf("Error: %v\n", statefulErr)
fmt.Printf("Attempts: %v\n", statefulErr.GetData("attempts"))
fmt.Printf("Last error: %v\n", statefulErr.GetData("lastError"))
// Output:
// Error: stateful error [processing]: processing error
// Attempts: 3
// Last error: timeout
// Try invalid transition
err = statefulErr.TransitionTo("pending")
if err != nil {
fmt.Printf("Invalid transition: %v\n", err)
}
// Output: Invalid transition: invalid transition from processing to pending
}
What You've Learned
Congratulations! You now have a comprehensive understanding of Go's custom error types:
Basic Custom Error Types
- Understanding how to create custom error types that implement the error interface
- Adding additional fields and context to error types
- Creating domain-specific errors for your application
- Implementing custom error methods and behavior
Error Classification and Hierarchies
- Organizing errors into categories and hierarchies
- Creating structured relationships between different error types
- Implementing error classification functions
- Building error handling systems based on error types
Advanced Custom Error Patterns
- Creating errors with rich context and metadata
- Implementing stateful errors with behavior
- Building error recovery and retry mechanisms
- Creating errors with stack traces and debugging information
Key Concepts
- Custom error types - User-defined types that implement the error interface
- Error classification - Organizing errors into categories for better handling
- Error hierarchies - Structured relationships between error types
- Contextual errors - Errors with rich context and metadata
- Stateful errors - Errors that can change state and provide behavior
Next Steps
You now have a solid foundation in Go's custom error types. In the next section, we'll explore panic and recover mechanisms, which are used for handling exceptional circumstances that cannot be handled with regular error returns.
Understanding custom error types is crucial for building robust, maintainable applications with proper error handling. These concepts form the foundation for all the more advanced error handling techniques we'll cover in the coming chapters.
Ready to learn about panic and recover? Let's explore Go's mechanisms for handling exceptional circumstances and learn when and how to use them effectively!