Go Error Handling
Go handles errors as regular values, not exceptions. A function that can fail returns an error as its last return value. The caller checks that error immediately after the function call. This approach makes error handling explicit, readable, and impossible to accidentally ignore.
The error Type
The built-in error type is an interface with one method: Error() string. A function returns nil when everything goes well, and a non-nil error when something fails.
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result) // Result: 5
}
result2, err2 := divide(10, 0)
if err2 != nil {
fmt.Println("Error:", err2) // Error: cannot divide by zero
}
}
Error Handling Flow
result, err := divide(10, 0)
│
▼
function runs
│
b == 0? Yes
│
returns (0, error)
│
┌─────────┴──────────┐
err != nil err == nil
│ │
handle error use result
Creating Errors
errors.New
import "errors"
err := errors.New("something went wrong")
fmt.Errorf – Formatted Error Messages
import "fmt"
age := -5
err := fmt.Errorf("invalid age: %d — must be 0 or greater", age)
Wrapping Errors with fmt.Errorf and %w
The %w verb wraps an existing error inside a new one, preserving the original for inspection later.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
func findUser(id int) error {
return fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
}
func main() {
err := findUser(42)
fmt.Println(err) // findUser(42): record not found
if errors.Is(err, ErrNotFound) {
fmt.Println("The original error is ErrNotFound") // printed
}
}
errors.Is – Checking Error Identity
errors.Is checks whether an error (or any error it wraps) matches a specific target error.
if errors.Is(err, ErrNotFound) {
// handle not-found specifically
}
errors.As – Checking Error Type
errors.As checks whether an error (or any wrapped error) is a specific type, and extracts it for inspection.
package main
import (
"errors"
"fmt"
)
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
}
func main() {
err := validate("")
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("Field:", ve.Field) // Field: name
fmt.Println("Issue:", ve.Message) // Issue: cannot be empty
}
}
The Standard Error Pattern
| Situation | What to Do |
|---|---|
| Simple error message | errors.New("message") |
| Error with context/values | fmt.Errorf("context: %w", err) |
| Check error identity | errors.Is(err, targetErr) |
| Check error type | errors.As(err, &target) |
| No error occurred | Return nil |
Key Points
- Errors in Go are values — return them like any other value
- Always check
if err != nilimmediately after a function that returns an error nilmeans success; a non-nil error means failure- Use
fmt.Errorfwith%wto wrap errors and preserve the original - Use
errors.Isanderrors.Asto inspect wrapped error chains
