Go Error Handling: Love It or Hate It?
Go’s error handling is controversial. Some people love it, some hate it. After 6 months of writing Go in production, here’s where I land.
The Go Way
In Go, errors are values. Functions return errors as their last return value:
func readFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return data, nil
}
You check errors explicitly:
data, err := readFile("config.json")
if err != nil {
log.Fatal(err)
}
// Use data
No exceptions, no try/catch. Just explicit error checking.
What I Like
1. Errors Are Visible
You can’t ignore errors in Go (well, you can with _, but it’s obvious):
data, _ := readFile("file.txt") // Ignoring error - code smell
In languages with exceptions, it’s easy to forget error handling:
// Java - might throw, might not, who knows?
String data = readFile("file.txt");
2. No Hidden Control Flow
Exceptions create hidden control flow. A function 10 levels deep can throw an exception that bubbles up. In Go, errors flow explicitly through your code.
3. Forces You to Think About Errors
Every error requires a decision: log it, return it, wrap it, handle it. This makes you think about error cases upfront.
What I Don’t Like
1. Repetitive Code
This pattern appears everywhere:
result1, err := doThing1()
if err != nil {
return err
}
result2, err := doThing2(result1)
if err != nil {
return err
}
result3, err := doThing3(result2)
if err != nil {
return err
}
It’s verbose. Really verbose. About 50% of my Go code is error checking.
2. Error Context Gets Lost
When you return an error up the stack, you lose context:
func processUser(id int) error {
user, err := getUser(id)
if err != nil {
return err // What was the ID? Where did this fail?
}
// ...
}
You have to manually add context:
if err != nil {
return fmt.Errorf("failed to get user %d: %v", id, err)
}
3. No Stack Traces
When an error occurs, you don’t get a stack trace. You have to add logging at each level to track where errors originate.
Patterns I Use
1. Wrap Errors with Context
func processOrder(orderID string) error {
order, err := getOrder(orderID)
if err != nil {
return fmt.Errorf("processOrder: failed to get order %s: %v", orderID, err)
}
err = validateOrder(order)
if err != nil {
return fmt.Errorf("processOrder: validation failed for order %s: %v", orderID, err)
}
return nil
}
This gives you a breadcrumb trail when errors occur.
2. Custom Error Types
For errors that need special handling:
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)
}
// Usage
if age < 0 {
return &ValidationError{Field: "age", Message: "must be positive"}
}
Then you can type-assert to handle specific errors:
if err != nil {
if valErr, ok := err.(*ValidationError); ok {
// Handle validation error specifically
log.Printf("Validation failed: %s", valErr.Field)
}
return err
}
3. Error Checking Helper
For cases where you just want to panic on error (like in main or tests):
func must(err error) {
if err != nil {
panic(err)
}
}
// Usage
data, err := ioutil.ReadFile("config.json")
must(err)
Don’t use this in library code, but it’s handy for applications.
Comparison with Other Languages
Java: Checked exceptions are annoying, unchecked exceptions are invisible. Go’s approach is better.
Python: Exceptions are fine, but easy to ignore. Go forces you to handle errors.
JavaScript: Callbacks made error handling messy. Promises help, but Go’s approach is more explicit.
Rust: Result types are similar to Go’s approach but with better type safety. I like Rust’s ? operator for propagating errors.
The Verdict
I’ve made peace with Go’s error handling. Yes, it’s verbose. Yes, it’s repetitive. But it’s also explicit, predictable, and forces you to think about error cases.
After debugging production issues in other languages where exceptions were swallowed or ignored, I appreciate Go’s approach.
Would I prefer something like Rust’s Result<T, E> with the ? operator? Maybe. But Go’s simplicity has its own appeal.
Tips for Go Error Handling
- Always check errors: Don’t use
_unless you have a good reason - Add context: Wrap errors with information about what failed
- Use custom error types: For errors that need special handling
- Log at boundaries: Log errors where they’re handled, not everywhere
- Don’t panic: Reserve panic for truly exceptional cases
What’s Coming
There’s talk of adding better error handling to Go 2. The community has proposed various solutions, but nothing’s decided yet. I’m curious to see where it goes.
For now, I’m sticking with the current approach. It works, even if it’s not perfect.
What do you think about Go’s error handling? Love it? Hate it? Let me know.