Errors in Go: Values, Not Exceptions
Go handles errors differently—no exceptions like in Python or Java. Instead, errors are returned as values, making code more explicit and predictable.
1. Go’s Error Philosophy
Errors are values. They are not disruptive control flow tools, but part of the normal data returned from functions.
No exceptions. There is no
try/catch
or stack unwinding. You handle errors where they occur.Explicit is better than implicit. Go prefers repetitive clarity (
if err != nil
) over hidden or magical error handling.
“Errors are just values. They’re part of the normal flow of control.” — Rob Pike, Go co-creator
2. The Basics: Returning Errors Explicitly
A function that can fail typically returns two values: the result (if any) and an error.
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
If
b
is zero, we return a custom error. Otherwise, the division proceeds anderr
isnil
.
3. Idiomatic Checking: if err != nil
if err != nil
This is the most common pattern in Go:
value, err := someFunc()
if err != nil {
// handle error
}
Why so repetitive?
You always see where errors might occur—no hidden control flow.
You decide how to handle each error, right after the function call.
4. Creating and Propagating Errors
Creating Errors
Use
errors.New("message")
for simple errors.Use
fmt.Errorf("context: %w", err)
to wrap errors with context.
import (
"errors"
"fmt"
)
func readConfig() error {
return errors.New("config file not found")
}
func loadApp() error {
err := readConfig()
if err != nil {
return fmt.Errorf("loadApp failed: %w", err)
}
return nil
}
%w
infmt.Errorf
wraps the original error, preserving the trace.
5. Sentinel Errors: Known, Comparable Values
You can define specific errors for comparison:
var ErrNotFound = errors.New("not found")
func findUser(id int) error {
// ...
return ErrNotFound
}
if errors.Is(err, ErrNotFound) {
// handle not found
}
Use
errors.Is()
for type-safe error comparison.
6. Custom Error Types
For richer error data, define your own error type:
type HTTPError struct {
Code int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}
func fetch() error {
return &HTTPError{Code: 404, Message: "Not Found"}
}
err := fetch()
if httpErr, ok := err.(*HTTPError); ok {
fmt.Println("Status code:", httpErr.Code)
}
Use
errors.As()
to check and extract custom error types.
7. Best Practices
Handle errors immediately or explicitly propagate them.
Never ignore errors (don’t use
_
unless you’re absolutely sure).Use
fmt.Errorf
for adding context.Prefer
errors.Is()
anderrors.As()
for type-safe matching.Keep error messages clear and user-focused.
Avoid panics for normal error handling (use only for truly unrecoverable situations).
8. Anti-Patterns
Don’t suppress errors: Always check and handle them.
Don’t use panic/recover for flow control: Reserve for bugs or truly exceptional cases.
Don’t rely on string-matching error messages: Use sentinel errors or custom types.
9. Real-World Example: Error Propagation in Cloud APIs
Suppose you’re building a cloud service that fetches metadata from a provider and saves it to a database. Each step can fail:
func fetchMetadata() (Metadata, error) { /* ... */ }
func saveToDB(data Metadata) error { /* ... */ }
func process() error {
data, err := fetchMetadata()
if err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
if err := saveToDB(data); err != nil {
return fmt.Errorf("db save failed: %w", err)
}
return nil
}
func main() {
if err := process(); err != nil {
log.Fatalf("process failed: %v", err)
}
}
Error propagation is explicit and context-aware.
Zero values ensure structs like
Metadata
are safe to use even before initialization.Short
if err != nil
patterns keep logic close to where failures can occur.defer
can be used for resource cleanup (e.g., closing files or network connections).
10. Go vs. Exception-Based Languages
No hidden control flow: Errors are handled where they occur, not by unwinding the stack.
No try/catch: You always see error handling logic.
No stack trace drama: You’re in control of every potential failure and its propagation.
Composability: Errors can be stored, wrapped, compared, or logged like any other value.
11. Practice & Conceptual Questions
Write a function that returns an error if a file does not exist.
Create a custom error type for HTTP errors and demonstrate type assertion.
Use
fmt.Errorf
to wrap an error with context and extract the original error usingerrors.Is
.Refactor a function to propagate errors up the call stack with context.
Show how to use
defer
for resource cleanup in the presence of errors.Compare Go’s error handling to exception-based languages. What are the trade-offs?
Further Reading
Last updated