Error Handling
## Learning Objectives
- Understand Go's error handling philosophy
- Work with the error interface
- Create custom errors
- Handle errors effectively
- Use panic and recover appropriately
## Go's Error Philosophy
Go uses explicit error returns rather than exceptions.
```go
result, err := riskyOperation()
if err != nil {
// Handle error
}
```
## The error Interface
```go
type error interface {
Error() string
}
```
### Basic Error
```go
import "errors"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
```
## Creating Errors
### errors.New
```go
import "errors"
err := errors.New("something went wrong")
fmt.Println(err) // something went wrong
```
### fmt.Errorf
```go
import "fmt"
err := fmt.Errorf("invalid input: %s", "missing value")
```
### Custom Error Types
```go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validate(name string) error {
if name == "" {
return &ValidationError{Field: "name", Message: "cannot be empty"}
}
return nil
}
```
### Error Wrapping (Go 1.13+)
```go
import "fmt"
func readConfig(path string) error {
err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}
return nil
}
```
### Unwrap
```go
import "errors"
err := readConfig("missing.txt")
if err != nil {
// Check original error
if os.IsNotExist(err) {
fmt.Println("File does not exist")
}
// Unwrap chain
for err != nil {
fmt.Println(err)
err = errors.Unwrap(err)
}
}
```
## Handling Errors
### Basic Pattern
```go
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
```
### Early Return
```go
func process(data string) error {
file, err := os.Open(data)
if err != nil {
return err // Early return on error
}
defer file.Close()
// Continue processing...
return nil
}
```
### Multiple Errors
```go
func processAll(files []string) error {
for _, file := range files {
if err := process(file); err != nil {
return fmt.Errorf("processing %s: %w", file, err)
}
}
return nil
}
```
## Error Checking Patterns
### Sentinel Errors
```go
import "errors"
import "os"
var ErrNotFound = errors.New("resource not found")
func find(id string) (string, error) {
if id == "missing" {
return "", ErrNotFound
}
return "found", nil
}
if err := find("missing"); err != nil {
if errors.Is(err, ErrNotFound) {
fmt.Println("Resource was not found")
}
}
```
### errors.Is
```go
import "errors"
err := find("missing")
if errors.Is(err, ErrNotFound) {
// Handle not found
}
```
### errors.As
```go
type CustomError struct {
Code int
Msg string
}
func (e *CustomError) Error() string {
return e.Msg
}
var cerr *CustomError
if errors.As(err, &cerr) {
fmt.Printf("Custom error code: %d\n", cerr.Code)
}
```
## Common Error Functions
### os Errors
```go
import "os"
file, err := os.Open("missing.txt")
if err != nil {
os.IsNotExist(err) // true if file doesn't exist
os.IsPermission(err) // true if permission denied
os.IsExist(err) // true if file already exists
}
```
### net Errors
```go
import "net"
_, err := net.Dial("tcp", "localhost:9999")
if err != nil {
var opErr *net.OpError
if errors.As(err, &opErr) {
fmt.Println("Operation:", opErr.Op)
}
}
```
## Custom Error Handling
### Error Handler
```go
type Handler func(error)
func handle(h Handler, err error) {
if err != nil {
h(err)
}
}
```
### Retry Pattern
```go
func withRetry(maxRetries int, fn func() error) error {
var err error
for i := 0; i < maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
time.Sleep(time.Duration(i+1) * time.Second)
}
return fmt.Errorf("after %d retries: %w", maxRetries, err)
}
```
## panic and recover
### When to Use panic
- Reserved for truly unrecoverable situations
- Programming errors (nil pointer, out of bounds)
- Not for expected error conditions
### panic
```go
func main() {
panic("something bad happened!")
fmt.Println("This won't run")
}
```
### recover
```go
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
}
}()
panic("Something went wrong!")
fmt.Println("This won't run")
}
func main() {
safeCall()
fmt.Println("Program continues...")
}
```
### Recover in Defer
```go
func mayPanic() {
panic("panic!")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Caught:", r)
}
}()
mayPanic()
fmt.Println("After mayPanic")
}
```
### Best Practice: Defer Recovery at Boundary
```go
func server() {
defer func() {
if r := recover(); r != nil {
log.Printf("Server panic: %v", r)
os.Exit(1)
}
}()
// Server logic...
}
```
## Log Errors
### log Package
```go
import "log"
if err != nil {
log.Printf("operation failed: %v", err)
}
if err != nil {
log.Fatalf("critical error: %v", err)
}
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
```
### Custom Logger
```go
logger := log.New(os.Stderr, "APP: ", log.Ldate|log.Ltime)
logger.Printf("Starting server on port %d", 8080)
logger.Fatalf("Failed to start: %v", err)
```
## Error Visibility
### Avoid Hidden Errors
```go
// BAD: ignores error
bytes, _ := json.Marshal(data)
// GOOD: handle error
bytes, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("marshaling data: %w", err)
}
```
### No Error Suppression
```go
// BAD: silently ignores errors
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
}
// else case ignored!
// GOOD
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
```
## Summary
- Go uses explicit error handling via return values
- The `error` interface has one method: `Error() string`
- Create errors with `errors.New()` or `fmt.Errorf()`
- Use `%w` verb to wrap errors
- `errors.Is()` and `errors.As()` for error inspection
- panic/recover reserved for truly exceptional situations
- Log or handle every error explicitly
- Never suppress errors silently
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →