Go Custom Errors
Custom errors carry more information than a plain string message. By creating a struct that implements the error interface, programs can attach structured data to an error — like which field failed, what the invalid value was, or what operation caused the problem. This makes error handling precise and machine-readable.
What Makes a Type an Error?
Any type that has an Error() string method satisfies the error interface.
type error interface {
Error() string
}
To create a custom error, define a struct and give it an Error() method.
Basic Custom Error
package main
import "fmt"
type AppError struct {
Code int
Message string
}
func (e *AppError) Error() string {
return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}
func processOrder(id int) error {
if id <= 0 {
return &AppError{Code: 400, Message: "order ID must be positive"}
}
return nil
}
func main() {
err := processOrder(-1)
if err != nil {
fmt.Println(err) // Error 400: order ID must be positive
}
}
Custom Error Structure Diagram
type AppError struct
┌──────────────────────────────┐
│ Code │ 400 │
│ Message │ "invalid input" │
└──────────────────────────────┘
│
└── Error() string method makes it an error interface
err.Error() → "Error 400: invalid input"
Sentinel Errors – Predefined Error Values
Sentinel errors are package-level error variables. They represent well-known error conditions that callers can check with errors.Is.
package main
import (
"errors"
"fmt"
)
var (
ErrNotFound = errors.New("item not found")
ErrPermission = errors.New("permission denied")
ErrTimeout = errors.New("operation timed out")
)
func fetchItem(id int) error {
if id == 0 {
return ErrNotFound
}
return nil
}
func main() {
err := fetchItem(0)
switch {
case errors.Is(err, ErrNotFound):
fmt.Println("Handle not found:", err)
case errors.Is(err, ErrPermission):
fmt.Println("Handle permission error:", err)
default:
fmt.Println("Unknown error:", err)
}
}
Rich Custom Error with Context
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field '%s' (value: %v): %s",
e.Field, e.Value, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Message: "age cannot be negative",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "age is unrealistically high",
}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
fmt.Println(err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("Field:", ve.Field)
fmt.Println("Value:", ve.Value)
}
}
}
Output:
validation failed on field 'age' (value: -5): age cannot be negative
Field: age
Value: -5
Wrapping Custom Errors
Custom errors can wrap other errors to preserve the original cause.
package main
import (
"errors"
"fmt"
)
type DBError struct {
Operation string
Err error
}
func (e *DBError) Error() string {
return fmt.Sprintf("database error during %s: %v", e.Operation, e.Err)
}
func (e *DBError) Unwrap() error {
return e.Err
}
var ErrConnection = errors.New("connection refused")
func queryDB() error {
return &DBError{
Operation: "SELECT",
Err: ErrConnection,
}
}
func main() {
err := queryDB()
fmt.Println(err)
if errors.Is(err, ErrConnection) {
fmt.Println("Root cause: connection refused") // works via Unwrap
}
}
Key Points
- Any type with an
Error() stringmethod satisfies theerrorinterface - Custom error structs carry structured data beyond a plain message
- Sentinel errors are package-level variables for well-known error conditions
- Implement
Unwrap() erroron a custom error to supporterrors.Isanderrors.Ason wrapped chains - Use
errors.Asto extract a custom error type from an error chain
