Error Basics & Custom Errors
Go treats errors as values, not exceptions. This simple but powerful approach makes error handling explicit and predictable. In this lesson, you'll learn Go's error philosophy, how to create and handle errors, and how to design custom error types.
The Error Interface
In Go, errors are just values that implement the error interface:
type error interface {
Error() string
}
Click Run to execute your code
- Errors are values - Not exceptions to be thrown/caught
- Explicit handling - Check errors where they occur
- Return errors - Functions return errors as values
- No hidden control flow - No try/catch blocks
Creating Errors
import "errors"
// Simple error
err := errors.New("something went wrong")
// Formatted error
import "fmt"
err := fmt.Errorf("invalid value: %d", value)
Error Handling Patterns
1. Check and Return
Click Run to execute your code
if err != nil { return err } is idiomatic Go.
2. Early Return
func processData(filename string) error {
data, err := readFile(filename)
if err != nil {
return err // Early return on error
}
result, err := transform(data)
if err != nil {
return err
}
err = save(result)
if err != nil {
return err
}
return nil // Success
}
3. Error with Context
func readConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &config, nil
}
Custom Error Types
Create custom errors by implementing the error interface:
Click Run to execute your code
- Need to include additional context (fields)
- Want to distinguish error types programmatically
- Need structured error information
- Building libraries or APIs
Sentinel Errors
Sentinel errors are predefined error values for specific conditions:
Click Run to execute your code
Standard Library Sentinel Errors
import (
"io"
"os"
)
// io package
io.EOF // End of file
io.ErrClosedPipe // Pipe closed
io.ErrUnexpectedEOF
// os package
os.ErrNotExist // File doesn't exist
os.ErrExist // File already exists
os.ErrPermission // Permission denied
errors.Is() to check for sentinel
errors, not
==. This works even when errors are wrapped!
Checking Error Types
Using errors.As()
import "errors"
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// err is a ValidationError
fmt.Println("Field:", validationErr.Field)
fmt.Println("Message:", validationErr.Message)
}
// Works with wrapped errors too!
err := fmt.Errorf("processing failed: %w", &ValidationError{
Field: "age",
Message: "must be positive",
})
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("Found:", ve.Field) // Works!
}
Common Error Patterns
1. Multiple Return Values
// Return result and error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Usage
result, err := divide(10, 0)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
2. Named Return Values
func readFile(name string) (data []byte, err error) {
f, err := os.Open(name)
if err != nil {
return // Returns nil, err
}
defer f.Close()
data, err = io.ReadAll(f)
return // Returns data, err
}
3. Error Aggregation
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
var msgs []string
for _, err := range m.Errors {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}
Common Mistakes
1. Ignoring errors
// โ Wrong - ignoring error
data, _ := os.ReadFile("config.json")
// โ
Correct - handle error
data, err := os.ReadFile("config.json")
if err != nil {
log.Fatal(err)
}
2. Using panic for normal errors
// โ Wrong - panic for expected errors
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
// โ
Correct - return error
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
3. Not adding context to errors
// โ Less helpful
func loadConfig() error {
_, err := os.ReadFile("config.json")
return err // Where did this error come from?
}
// โ
Better - add context
func loadConfig() error {
_, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
return nil
}
4. Comparing errors with ==
// โ Wrong - doesn't work with wrapped errors
if err == io.EOF {
// Won't match if err is wrapped
}
// โ
Correct - use errors.Is()
if errors.Is(err, io.EOF) {
// Works even if wrapped!
}
Exercise: User Validation
Task: Create a user validation system with custom errors.
Requirements:
- Create a
ValidationErrortype with Field and Message - Create a
validateUserfunction - Validate: name not empty, age >= 0, email contains @
- Return appropriate validation errors
Show Solution
package main
import (
"errors"
"fmt"
"strings"
)
// ValidationError represents a validation failure
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// User represents a user
type User struct {
Name string
Age int
Email string
}
// validateUser validates a user
func validateUser(u User) error {
if u.Name == "" {
return &ValidationError{
Field: "name",
Message: "cannot be empty",
}
}
if u.Age < 0 {
return &ValidationError{
Field: "age",
Message: "must be non-negative",
}
}
if !strings.Contains(u.Email, "@") {
return &ValidationError{
Field: "email",
Message: "must contain @",
}
}
return nil
}
func main() {
// Test cases
users := []User{
{Name: "Alice", Age: 25, Email: "[email protected]"},
{Name: "", Age: 30, Email: "[email protected]"},
{Name: "Carol", Age: -5, Email: "[email protected]"},
{Name: "Dave", Age: 40, Email: "invalid-email"},
}
for i, user := range users {
fmt.Printf("Validating user %d: %+v\n", i+1, user)
err := validateUser(user)
if err != nil {
// Check if it's a ValidationError
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf(" โ Invalid %s: %s\n", ve.Field, ve.Message)
} else {
fmt.Printf(" โ Error: %v\n", err)
}
} else {
fmt.Println(" โ
Valid")
}
fmt.Println()
}
}
Summary
- Errors are values in Go, not exceptions
- error interface requires only Error() string method
- Check errors explicitly with if err != nil
- Create errors with errors.New() or fmt.Errorf()
- Custom errors implement the error interface
- Sentinel errors are predefined error values
- errors.Is() checks for specific errors
- errors.As() extracts custom error types
What's Next?
Now that you understand error basics and custom errors, you're ready to learn about Error Wrapping & Best Practices. You'll discover how to wrap errors for better context and explore advanced error handling patterns.
Enjoying these tutorials?