Custom Errors and Error Wrapping
Go’s error handling philosophy is simple: errors are values. This guide covers creating custom error types and wrapping errors with context, which are essential for robust error handling in Go.
Basic Error Interface
Understanding the Error Interface
package main
import "fmt"
// The error interface is defined as:
// type error interface {
// Error() string
// }
// Any type that implements Error() string is an error
type MyError struct {
Message string
}
func (e *MyError) Error() string {
return e.Message
}
func main() {
var err error = &MyError{Message: "something went wrong"}
fmt.Println(err) // something went wrong
}
Creating Custom Error Types
Simple Custom Error
package mypackage
import "fmt"
// ValidationError represents a validation error
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}
func ValidateEmail(email string) error {
if email == "" {
return &ValidationError{
Field: "email",
Message: "email is required",
}
}
if !contains(email, "@") {
return &ValidationError{
Field: "email",
Message: "email must contain @",
}
}
return nil
}
func contains(s, substr string) bool {
// Implementation
return true
}
Error with Additional Context
package database
import "fmt"
// DatabaseError represents a database error
type DatabaseError struct {
Operation string
Table string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error: %s on table %s: %v", e.Operation, e.Table, e.Err)
}
// Unwrap returns the underlying error
func (e *DatabaseError) Unwrap() error {
return e.Err
}
func GetUser(id int) error {
// Simulate database error
err := fmt.Errorf("connection refused")
return &DatabaseError{
Operation: "SELECT",
Table: "users",
Err: err,
}
}
Error with Stack Trace
package mypackage
import (
"fmt"
"runtime"
)
// ErrorWithStack represents an error with stack trace
type ErrorWithStack struct {
Message string
Stack string
}
func (e *ErrorWithStack) Error() string {
return fmt.Sprintf("%s\nStack:\n%s", e.Message, e.Stack)
}
func NewErrorWithStack(message string) *ErrorWithStack {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
return &ErrorWithStack{
Message: message,
Stack: string(buf[:n]),
}
}
Error Wrapping
Using fmt.Errorf with %w
package main
import (
"fmt"
"os"
)
func main() {
// Wrap error with context
file, err := os.Open("nonexistent.txt")
if err != nil {
// โ
Good: Wrap error with context
wrappedErr := fmt.Errorf("failed to open file: %w", err)
fmt.Println(wrappedErr)
// โ Bad: Lose error context
// fmt.Println(err)
}
}
Unwrapping Errors
package main
import (
"errors"
"fmt"
"os"
)
func main() {
file, err := os.Open("nonexistent.txt")
if err != nil {
wrappedErr := fmt.Errorf("failed to open file: %w", err)
// Unwrap to get original error
originalErr := errors.Unwrap(wrappedErr)
fmt.Println("Original:", originalErr)
// Check if it's a specific error
if errors.Is(wrappedErr, os.ErrNotExist) {
fmt.Println("File not found")
}
}
}
Error Chain
package main
import (
"errors"
"fmt"
)
func operation1() error {
return fmt.Errorf("operation1 failed")
}
func operation2() error {
err := operation1()
if err != nil {
return fmt.Errorf("operation2 failed: %w", err)
}
return nil
}
func operation3() error {
err := operation2()
if err != nil {
return fmt.Errorf("operation3 failed: %w", err)
}
return nil
}
func main() {
err := operation3()
if err != nil {
fmt.Println(err)
// Output: operation3 failed: operation2 failed: operation1 failed
// Unwrap the chain
for err != nil {
fmt.Println("Error:", err)
err = errors.Unwrap(err)
}
}
}
Error Checking
Using errors.Is
package main
import (
"errors"
"fmt"
"os"
)
func main() {
file, err := os.Open("nonexistent.txt")
if err != nil {
wrappedErr := fmt.Errorf("failed to open file: %w", err)
// โ
Good: Use errors.Is for wrapped errors
if errors.Is(wrappedErr, os.ErrNotExist) {
fmt.Println("File not found")
}
// โ Bad: Direct comparison doesn't work with wrapped errors
// if wrappedErr == os.ErrNotExist {
// fmt.Println("File not found")
// }
}
}
Using errors.As
package main
import (
"errors"
"fmt"
"os"
)
func main() {
file, err := os.Open("nonexistent.txt")
if err != nil {
wrappedErr := fmt.Errorf("failed to open file: %w", err)
// Extract specific error type
var pathErr *os.PathError
if errors.As(wrappedErr, &pathErr) {
fmt.Printf("Path error: %s\n", pathErr.Path)
}
}
}
Practical Examples
Validation Error
package validation
import "fmt"
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
func ValidateUser(name, email string) error {
if name == "" {
return &ValidationError{
Field: "name",
Message: "name is required",
}
}
if email == "" {
return &ValidationError{
Field: "email",
Message: "email is required",
}
}
return nil
}
func main() {
err := ValidateUser("", "[email protected]")
if err != nil {
fmt.Println(err) // validation error: name - name is required
}
}
Database Error
package database
import (
"fmt"
)
type DatabaseError struct {
Operation string
Table string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error: %s on %s: %v", e.Operation, e.Table, e.Err)
}
func (e *DatabaseError) Unwrap() error {
return e.Err
}
func GetUser(id int) error {
// Simulate database error
err := fmt.Errorf("connection refused")
return &DatabaseError{
Operation: "SELECT",
Table: "users",
Err: err,
}
}
func main() {
err := GetUser(1)
if err != nil {
fmt.Println(err)
// Output: database error: SELECT on users: connection refused
}
}
API Error
package api
import (
"fmt"
)
type APIError struct {
StatusCode int
Message string
Err error
}
func (e *APIError) Error() string {
if e.Err != nil {
return fmt.Sprintf("API error %d: %s (%v)", e.StatusCode, e.Message, e.Err)
}
return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Message)
}
func (e *APIError) Unwrap() error {
return e.Err
}
func CallAPI() error {
// Simulate API error
return &APIError{
StatusCode: 500,
Message: "Internal Server Error",
Err: fmt.Errorf("database connection failed"),
}
}
func main() {
err := CallAPI()
if err != nil {
fmt.Println(err)
// Output: API error 500: Internal Server Error (database connection failed)
}
}
Error Handling Patterns
Sentinel Errors
package mypackage
import "errors"
// Define sentinel errors
var (
ErrNotFound = errors.New("not found")
ErrInvalid = errors.New("invalid")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id int) error {
if id <= 0 {
return ErrInvalid
}
// Simulate not found
return ErrNotFound
}
func main() {
err := GetUser(0)
if err == ErrInvalid {
fmt.Println("Invalid ID")
}
}
Error Wrapping with Sentinel
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func GetUser(id int) error {
if id <= 0 {
return fmt.Errorf("invalid id: %w", ErrNotFound)
}
return nil
}
func main() {
err := GetUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found")
}
}
Best Practices
โ Good Practices
- Implement Error() method - For custom error types
- Wrap errors with context - Use fmt.Errorf with %w
- Use errors.Is and errors.As - For error checking
- Implement Unwrap() method - For error chains
- Use sentinel errors - For specific error types
- Add context to errors - Include relevant information
- Don’t ignore errors - Always handle them
- Log errors appropriately - Include stack traces when needed
โ Anti-Patterns
// โ Bad: Ignore errors
file, _ := os.Open("file.txt")
// โ
Good: Handle errors
file, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
// โ Bad: Lose error context
return err
// โ
Good: Wrap with context
return fmt.Errorf("operation failed: %w", err)
// โ Bad: Direct comparison with wrapped errors
if wrappedErr == os.ErrNotExist {
// Won't work
}
// โ
Good: Use errors.Is
if errors.Is(wrappedErr, os.ErrNotExist) {
// Works correctly
}
Summary
Custom errors and error wrapping are essential:
- Implement custom error types
- Wrap errors with context
- Use errors.Is and errors.As
- Implement Unwrap() for error chains
- Use sentinel errors for specific types
- Always handle errors appropriately
- Add meaningful context to errors
Master error handling for robust Go applications.
Comments