Lesson 9 of 12

Errors, Panics & Testing

Learn how Go reports as ordinary return values, when panic is appropriate, and how to write tests with the built-in testing .

Go
package main

import (
	"errors"
	"fmt"
	"os"
)

// Sentinel errors — comparable values callers can test for
var ErrNotFound = errors.New("not found")

// Custom error types carry structured detail
type ValidationError struct {
	Field  string
	Reason string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("field %q is invalid: %s", e.Field, e.Reason)
}

func findUser(id int) (string, error) {
	users := map[int]string{1: "Ada"}
	name, ok := users[id]
	if !ok {
		return "", fmt.Errorf("findUser %d: %w", id, ErrNotFound)   // %w wraps
	}
	return name, nil
}

func main() {
	_, err := findUser(99)

	// errors.Is unwraps the chain looking for a specific value
	if errors.Is(err, ErrNotFound) {
		fmt.Println("no such user")
	}

	// errors.As unwraps looking for a specific TYPE, and binds it
	var verr *ValidationError
	if errors.As(err, &verr) {
		fmt.Println("bad field:", verr.Field)
	}

	if _, err := os.Open("missing.txt"); errors.Is(err, os.ErrNotExist) {
		fmt.Println("file is not there")
	}
}
Go
package main

import (
	"fmt"
	"os"
)

// defer runs when the function returns — even on panic. LIFO order.
func processFile(path string) (err error) {
	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("open %s: %w", path, err)
	}
	defer f.Close()                 // guaranteed cleanup

	// Named return + recover turns a panic back into an error
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()

	// ... work that might panic ...
	return nil
}

// Panic is for programmer error and unrecoverable state, NOT control flow.
func mustPositive(n int) int {
	if n <= 0 {
		panic("n must be positive")     // a bug in the caller, not a runtime condition
	}
	return n
}

func main() {
	if err := processFile("nope.txt"); err != nil {
		fmt.Println("error:", err)
	}
}
Go
// calc.go
package calc

import "errors"

func Add(a, b int) int { return a + b }

func Divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

// calc_test.go — file name must end in _test.go
package calc

import "testing"

func TestAdd(t *testing.T) {
	if got := Add(2, 3); got != 5 {
		t.Errorf("Add(2,3) = %d, want 5", got)
	}
}

// Table-driven tests are the Go idiom
func TestDivide(t *testing.T) {
	tests := []struct {
		name    string
		a, b    float64
		want    float64
		wantErr bool
	}{
		{"simple", 10, 2, 5, false},
		{"fraction", 1, 4, 0.25, false},
		{"by zero", 1, 0, 0, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {      // subtest: named, isolated
			got, err := Divide(tt.a, tt.b)
			if (err != nil) != tt.wantErr {
				t.Fatalf("unexpected error state: %v", err)
			}
			if !tt.wantErr && got != tt.want {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}
}

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ { Add(1, 2) }
}

// go test ./...            run everything
// go test -v -run Divide   verbose, only matching tests
// go test -cover           coverage
// go test -race            race detector
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.