Error Handling
Introduction
Go handles errors in a fundamentally different way from Python. Python uses exceptions — when something goes wrong, a function raises an exception, and the caller catches it with try/except. The error handling can be placed far from the code that might fail.
Go uses error values — a function that can fail returns an error as its last return value. The caller checks if err != nil immediately after the call. This makes error handling explicit and local: you can see exactly where failures might occur just by reading the code.
The error type in Go is a built-in interface with a single method: Error() string. Any type that implements this method can be used as an error. The standard library provides errors.New() and fmt.Errorf() for creating simple errors.
This approach has trade-offs. Go error handling is more verbose than Python's try/except, but it makes code easier to reason about in large systems — you always know which functions can fail, and failures are impossible to accidentally ignore (the compiler will warn you about unused return values).
Concepts
The error interface
// The built-in error interface:
type error interface {
Error() string
}
Creating errors
import (
"errors"
"fmt"
)
// Simple error
err1 := errors.New("something went wrong")
// Formatted error
err2 := fmt.Errorf("value %d is out of range", 42)
Checking errors
result, err := doSomething()
if err != nil {
// handle the error
return err // or log it, or wrap it
}
// use result safely here
Custom error types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
Wrapping errors
// Wrap an error with context (Go 1.13+)
if err != nil {
return fmt.Errorf("processing user %d: %w", userID, err)
}
// Check the type of error
var ErrNotFound := errors.New("Could not find requested resource")
resource, err := getResource("/missing-resource") // returns an ErrNotFound
if errors.Is(err, ErrNotFound) {
fmt.Println("Could not find resource")
}
// Unwrap
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("Field:", valErr.Field)
}
Examples
package main
import (
"errors"
"fmt"
"strconv"
)
func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("invalid age %q: %w", s, err)
}
if age < 0 || age > 150 {
return 0, fmt.Errorf("age %d is out of valid range (0-150)", age)
}
return age, nil
}
func main() {
inputs := []string{"25", "abc", "-5", "200", "42"}
for _, input := range inputs {
age, err := parseAge(input)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Age: %d\n", age)
}
}
}
Challenges
1. Basic error return
- Write a function
sqrt(n float64) (float64, error)that returns an error ifnis negative. - Test it with positive and negative values.
💡 Show Answer
package main
import (
"fmt"
"math"
)
func sqrt(n float64) (float64, error) {
if n < 0 {
return 0, fmt.Errorf("cannot take square root of negative number: %v", n)
}
return math.Sqrt(n), nil
}
func main() {
values := []float64{16, -4, 2, 0, -100}
for _, v := range values {
result, err := sqrt(v)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("sqrt(%.1f) = %.4f\n", v, result)
}
}
}
2. Propagating errors
- Write a function
processNumber(s string) (float64, error)that:- Parses the string as a float using
strconv.ParseFloat. - Calls your
sqrtfunction from challenge 1. - Returns any errors wrapped with context using
fmt.Errorf(..., %w, err).
- Parses the string as a float using
💡 Show Answer
package main
import (
"fmt"
"math"
"strconv"
)
func sqrt(n float64) (float64, error) {
if n < 0 {
return 0, fmt.Errorf("cannot take sqrt of negative: %v", n)
}
return math.Sqrt(n), nil
}
func processNumber(s string) (float64, error) {
n, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, fmt.Errorf("processNumber: parsing %q: %w", s, err)
}
result, err := sqrt(n)
if err != nil {
return 0, fmt.Errorf("processNumber: %w", err)
}
return result, nil
}
func main() {
for _, s := range []string{"16", "abc", "-4", "2.25"} {
result, err := processNumber(s)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %.4f\n", result)
}
}
}
3. Custom error type
- Create a
DivisionErrorstruct with fieldsNumeratorandDenominator. - Implement the
errorinterface on it. - Use it in a
dividefunction.
💡 Show Answer
package main
import "fmt"
type DivisionError struct {
Numerator float64
Denominator float64
}
func (e *DivisionError) Error() string {
return fmt.Sprintf("cannot divide %.2f by %.2f", e.Numerator, e.Denominator)
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, &DivisionError{Numerator: a, Denominator: b}
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
4. errors.As — type-checking errors
- Using your
DivisionErrorfrom challenge 3, callerrors.Asto check whether an error is a*DivisionErrorand print its fields if so.
💡 Show Answer
import "errors"
_, err := divide(10, 0)
if err != nil {
var divErr *DivisionError
if errors.As(err, &divErr) {
fmt.Printf("Caught division error: numerator=%.2f\n", divErr.Numerator)
} else {
fmt.Println("Unknown error:", err)
}
}
5. Compare to Python try/except
- Here is a Python function with exception handling:
def parse_and_sqrt(s):
try:
n = float(s)
if n < 0:
raise ValueError(f"Cannot take sqrt of {n}")
return n ** 0.5
except ValueError as e:
print(f"Error: {e}")
return None
- Ask Copilot to convert this to idiomatic Go.
- Note the structural difference: Go's error checking is at each call site rather than wrapped in a single block.
💡 Show Answer
package main
import (
"fmt"
"math"
"strconv"
)
func parseAndSqrt(s string) (float64, error) {
n, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, fmt.Errorf("parseAndSqrt: %w", err)
}
if n < 0 {
return 0, fmt.Errorf("cannot take sqrt of %v", n)
}
return math.Sqrt(n), nil
}
func main() {
result, err := parseAndSqrt("16.0")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("%.4f\n", result)
}
}
The Go version has if err != nil after each step rather than one catch block at the end. This is more verbose but makes it immediately clear which specific operation can fail.
6. panic and recover
- Ask Copilot: When should I use
panicandrecoverin Go instead of returning an error? - Write a short example using
panicandrecover.
💡 Show Answer
package main
import "fmt"
func riskyOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("something went badly wrong!")
}
func main() {
riskyOperation()
fmt.Println("Program continues after recovery")
}
Use panic only for truly unrecoverable situations — programmer errors, broken invariants, or conditions that should never occur in a correctly written program. For expected failure conditions (bad input, network errors, file not found), always use error values. This mirrors Python's convention of reserving exceptions for exceptional conditions, but Go takes it further by making the distinction part of the language's idiomatic style.