Error Basics
In Go, error handling is done through return values rather than exceptions. The convention is to return an error as the last return value when a function can fail. This simple design makes code predictable and explicit.
The error Interfaceβ
error is a built-in Go interface with just one method.
package main
import (
"errors"
"fmt"
)
// The error interface definition (Go standard library)
// type error interface {
// Error() string
// }
// errors.New β simplest way to create an error
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func findUser(id int) (string, error) {
users := map[int]string{
1: "Alice",
2: "Bob",
}
name, ok := users[id]
if !ok {
return "", ErrNotFound
}
return name, nil
}
func main() {
// Success case
name, err := findUser(1)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("user:", name) // user: Alice
}
// Failure case
name, err = findUser(99)
if err != nil {
fmt.Println("error:", err) // error: not found
} else {
fmt.Println("user:", name)
}
// Error comparison (sentinel error)
if err == ErrNotFound {
fmt.Println("user not found") // user not found
}
}
fmt.Errorf β Formatted Error Messagesβ
Use fmt.Errorf when you need error messages containing dynamic information.
package main
import (
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero: %v / %v", a, b)
}
return a / b, nil
}
func openFile(path string) error {
// Simulation: fail on certain paths
if path == "" {
return fmt.Errorf("openFile: path cannot be empty")
}
if path == "/etc/shadow" {
return fmt.Errorf("openFile %q: permission denied", path)
}
return nil
}
func main() {
// Using divide
result, err := divide(10, 0)
if err != nil {
fmt.Println("divide failed:", err) // divide failed: divide by zero: 10 / 0
} else {
fmt.Println("result:", result)
}
result, err = divide(10, 3)
if err != nil {
fmt.Println("divide failed:", err)
} else {
fmt.Printf("result: %.4f\n", result) // result: 3.3333
}
// Using openFile
for _, path := range []string{"", "/etc/shadow", "/tmp/data.txt"} {
if err := openFile(path); err != nil {
fmt.Println("file open failed:", err)
} else {
fmt.Printf("file open success: %q\n", path)
}
}
// file open failed: openFile: path cannot be empty
// file open failed: openFile "/etc/shadow": permission denied
// file open success: "/tmp/data.txt"
}
Error Return Conventionsβ
Conventions to follow when returning errors in Go.
package main
import (
"errors"
"fmt"
)
// Convention 1: error is always the last return value
func readConfig(path string) (string, error) {
if path == "" {
return "", errors.New("config path is required")
}
return "config content", nil
}
// Convention 2: return nil on success
func processData(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("data is empty")
}
result := make([]byte, len(data))
copy(result, data)
return result, nil
}
// Convention 3: when error occurs, other return values are meaningless (use zero value)
func parseInt(s string) (int, error) {
if s == "" {
return 0, errors.New("empty string") // 0 is a meaningless zero value
}
var n int
for _, c := range s {
if c < '0' || c > '9' {
return 0, fmt.Errorf("invalid character: %c", c)
}
n = n*10 + int(c-'0')
}
return n, nil
}
// Convention 4: error variable is named err (single), or descriptive names (multiple)
func multipleOperations() error {
_, err := readConfig("app.yaml")
if err != nil {
return err
}
_, err = processData([]byte("data"))
if err != nil {
return err
}
return nil
}
// Convention 5: package-level sentinel errors use Err prefix
var (
ErrEmpty = errors.New("empty input")
ErrInvalid = errors.New("invalid input")
ErrTimeout = errors.New("operation timed out")
)
func validate(input string) error {
if input == "" {
return ErrEmpty
}
if len(input) > 100 {
return ErrInvalid
}
return nil
}
func main() {
// Pattern 1: immediate if err != nil check
config, err := readConfig("app.yaml")
if err != nil {
fmt.Println("config load failed:", err)
return
}
fmt.Println("config:", config)
// Pattern 2: inline error handling
if n, err := parseInt("123"); err != nil {
fmt.Println("parse failed:", err)
} else {
fmt.Println("parse success:", n) // parse success: 123
}
if _, err := parseInt("12a3"); err != nil {
fmt.Println("parse failed:", err) // parse failed: invalid character: a
}
// Pattern 3: sentinel error comparison
inputs := []string{"", "valid", "too_long_input_exceeding_hundred_characters_limit_yes_this_is_very_long_string_here_to_test"}
for _, s := range inputs {
if err := validate(s); err != nil {
switch err {
case ErrEmpty:
fmt.Println("empty input")
case ErrInvalid:
fmt.Println("invalid input")
default:
fmt.Println("unknown error:", err)
}
} else {
fmt.Printf("valid input: %q\n", s)
}
}
}
Error and nil Handlingβ
Patterns for correctly propagating and handling errors.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
var ErrDBConn = errors.New("database connection failed")
// Layered error propagation β each layer passes the error up
type DB struct{ connected bool }
func (db *DB) Query(id int) (string, error) {
if !db.connected {
return "", ErrDBConn
}
if id <= 0 {
return "", ErrNotFound
}
return fmt.Sprintf("record-%d", id), nil
}
type Repository struct{ db *DB }
func (r *Repository) FindByID(id int) (string, error) {
record, err := r.db.Query(id)
if err != nil {
return "", err // propagate error as-is
}
return record, nil
}
type Service struct{ repo *Repository }
func (s *Service) GetUser(id int) (string, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return "", err // propagate error as-is
}
return "User: " + user, nil
}
func main() {
// Normal operation
db := &DB{connected: true}
repo := &Repository{db: db}
svc := &Service{repo: repo}
user, err := svc.GetUser(42)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println(user) // User: record-42
}
// DB connection failure
db2 := &DB{connected: false}
repo2 := &Repository{db: db2}
svc2 := &Service{repo: repo2}
_, err = svc2.GetUser(42)
if err != nil {
fmt.Println("error:", err) // error: database connection failed
if errors.Is(err, ErrDBConn) {
fmt.Println("β DB connection error detected") // β DB connection error detected
}
}
// Record not found
_, err = svc.GetUser(-1)
if err != nil {
fmt.Println("error:", err) // error: not found
if errors.Is(err, ErrNotFound) {
fmt.Println("β record not found") // β record not found
}
}
}
Collecting Multiple Errorsβ
Pattern for collecting errors from multiple operations and handling them together.
package main
import (
"errors"
"fmt"
"strings"
)
// MultiError β collects multiple errors into one type
type MultiError struct {
Errors []error
}
func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}
func (m *MultiError) Error() string {
msgs := make([]string, len(m.Errors))
for i, err := range m.Errors {
msgs[i] = err.Error()
}
return strings.Join(msgs, "; ")
}
func (m *MultiError) Unwrap() []error {
return m.Errors
}
func (m *MultiError) HasError() bool {
return len(m.Errors) > 0
}
// Validation β collect all errors and return at once
type UserInput struct {
Name string
Email string
Age int
}
func validateUser(u UserInput) error {
var merr MultiError
if u.Name == "" {
merr.Add(errors.New("name is required"))
} else if len(u.Name) < 2 {
merr.Add(errors.New("name must be at least 2 characters"))
}
if u.Email == "" {
merr.Add(errors.New("email is required"))
} else if !strings.Contains(u.Email, "@") {
merr.Add(errors.New("email must contain @"))
}
if u.Age < 0 {
merr.Add(errors.New("age cannot be negative"))
} else if u.Age > 150 {
merr.Add(errors.New("age is too large"))
}
if merr.HasError() {
return &merr
}
return nil
}
func main() {
inputs := []UserInput{
{Name: "Alice", Email: "alice@example.com", Age: 30},
{Name: "", Email: "invalid", Age: -1},
{Name: "B", Email: "bob@example.com", Age: 200},
}
for _, input := range inputs {
err := validateUser(input)
if err != nil {
fmt.Printf("validation failed (%+v):\n %v\n", input, err)
// Type assert to MultiError
var merr *MultiError
if errors.As(err, &merr) {
fmt.Printf(" total %d error(s)\n", len(merr.Errors))
}
} else {
fmt.Printf("valid input: %+v\n", input)
}
}
}
Key Summary
erroris an interface with a single method (Error() string)- Use
errors.Newfor static errors,fmt.Errorffor dynamic errors- Error is always the last return value; return
nilon success- Package-level sentinel errors use
Errprefix (e.g.,ErrNotFound)- Never ignore errors β discarding with
_is dangerous