Skip to main content

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 if n is 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:
    1. Parses the string as a float using strconv.ParseFloat.
    2. Calls your sqrt function from challenge 1.
    3. Returns any errors wrapped with context using fmt.Errorf(..., %w, err).
💡 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 DivisionError struct with fields Numerator and Denominator.
  • Implement the error interface on it.
  • Use it in a divide function.
💡 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 DivisionError from challenge 3, call errors.As to check whether an error is a *DivisionError and 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 panic and recover in Go instead of returning an error?
  • Write a short example using panic and recover.
💡 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.