Error Handling Basics — Tasks¶
Hands-on exercises. Each comes with a problem statement, hints, and a reference solution. Difficulty: easy → hard.
Task 1 (Easy) — Safe division¶
Write a function Divide(a, b float64) (float64, error) that returns a/b if b != 0, otherwise an error with the message "division by zero".
Hints - Return 0 and an error when b == 0. - Use errors.New.
Solution
package main
import (
"errors"
"fmt"
)
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
fmt.Println(Divide(10, 2))
fmt.Println(Divide(10, 0))
}
Task 2 (Easy) — Parse a positive integer¶
Write ParsePositive(s string) (int, error) that: - Returns the parsed integer if s is a valid positive number. - Returns an error if s is not a valid number, or if it is negative or zero.
Hints - Use strconv.Atoi. - Compose: parse first, then range-check.
Solution
func ParsePositive(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse %q: %w", s, err)
}
if n <= 0 {
return 0, fmt.Errorf("not positive: %d", n)
}
return n, nil
}
Task 3 (Easy) — Read a file or default¶
Write ReadOrDefault(path, def string) string that returns the contents of path, or def if the file cannot be read.
Hints - os.ReadFile. - Convert []byte to string.
Solution
func ReadOrDefault(path, def string) string {
data, err := os.ReadFile(path)
if err != nil {
return def
}
return string(data)
}
Task 4 (Easy → Medium) — First success¶
Write FirstSuccess(fns ...func() (int, error)) (int, error) that: - Calls each function in order. - Returns the first successful result. - If all fail, returns errors.Join of all errors.
Hints - Loop, accumulate, return on success.
Solution
func FirstSuccess(fns ...func() (int, error)) (int, error) {
var errs []error
for _, fn := range fns {
n, err := fn()
if err == nil {
return n, nil
}
errs = append(errs, err)
}
return 0, errors.Join(errs...)
}
Task 5 (Medium) — Validation with multiple errors¶
Write Validate(name, email string, age int) error that returns: - An error with all violated rules joined. - nil if everything is valid.
Rules: - name must be non-empty. - email must contain '@'. - age must be in [0, 150].
Solution
func Validate(name, email string, age int) error {
var errs []error
if name == "" {
errs = append(errs, errors.New("name: empty"))
}
if !strings.Contains(email, "@") {
errs = append(errs, errors.New("email: missing @"))
}
if age < 0 || age > 150 {
errs = append(errs, fmt.Errorf("age: out of range %d", age))
}
return errors.Join(errs...)
}
Task 6 (Medium) — Retry with backoff¶
Write Retry(attempts int, fn func() error) error that: - Calls fn up to attempts times. - Returns nil on first success. - Sleeps 100ms * 2^i between attempts. - Returns the last error wrapped with "after N attempts: %w".
Solution
func Retry(attempts int, fn func() error) error {
var lastErr error
for i := 0; i < attempts; i++ {
if err := fn(); err == nil {
return nil
} else {
lastErr = err
time.Sleep(100 * time.Millisecond << i)
}
}
return fmt.Errorf("after %d attempts: %w", attempts, lastErr)
}
Task 7 (Medium) — Sentinel error¶
Define var ErrEmpty = errors.New("empty input") and write Head(items []int) (int, error) that returns: - The first item on success. - ErrEmpty (wrapped) if the slice is empty.
The caller should be able to do if errors.Is(err, ErrEmpty).
Solution
var ErrEmpty = errors.New("empty input")
func Head(items []int) (int, error) {
if len(items) == 0 {
return 0, fmt.Errorf("Head: %w", ErrEmpty)
}
return items[0], nil
}
Task 8 (Medium) — Typed error with errors.As¶
Define ValidationError with Field and Message fields and Error() string method. Write RequireField(name, value string) error that returns a *ValidationError when value == "", nil otherwise.
A caller should be able to do errors.As(err, &ve) and then read ve.Field.
Solution
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
func RequireField(name, value string) error {
if value == "" {
return &ValidationError{Field: name, Message: "must not be empty"}
}
return nil
}
Task 9 (Medium → Hard) — Wrap-aware FileError¶
Define FileError carrying Path and underlying Err. Implement Error() and Unwrap(). Write OpenFile(path string) error that wraps a fake "not found" error.
Verify that errors.Is(err, fs.ErrNotExist) works.
Solution
import (
"errors"
"fmt"
"io/fs"
)
type FileError struct {
Path string
Err error
}
func (e *FileError) Error() string {
return fmt.Sprintf("file %q: %v", e.Path, e.Err)
}
func (e *FileError) Unwrap() error { return e.Err }
func OpenFile(path string) error {
return &FileError{Path: path, Err: fs.ErrNotExist}
}
func main() {
err := OpenFile("/nope")
fmt.Println(errors.Is(err, fs.ErrNotExist)) // true
}
Task 10 (Hard) — Multi-step pipeline with context propagation¶
Build Pipeline(input string) (Output, error) that runs: 1. parse(input) (Parsed, error) 2. validate(Parsed) error 3. transform(Parsed) (Output, error)
Each error must be wrapped with the step name. Top-level error message must look like:
Solution
type Parsed struct{ /* ... */ }
type Output struct{ /* ... */ }
func Pipeline(input string) (Output, error) {
p, err := parse(input)
if err != nil {
return Output{}, fmt.Errorf("Pipeline: parse: %w", err)
}
if err := validate(p); err != nil {
return Output{}, fmt.Errorf("Pipeline: validate: %w", err)
}
out, err := transform(p)
if err != nil {
return Output{}, fmt.Errorf("Pipeline: transform: %w", err)
}
return out, nil
}
Task 11 (Hard) — Concurrent fan-out with errgroup¶
Given a slice of URLs, fetch all of them in parallel with golang.org/x/sync/errgroup. Return a slice of bodies on success, or the first error.
Solution
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
bodies := make([][]byte, len(urls))
g, ctx := errgroup.WithContext(ctx)
for i, u := range urls {
i, u := i, u
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return fmt.Errorf("build req %d: %w", i, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch %s: %w", u, err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read %s: %w", u, err)
}
bodies[i] = b
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return bodies, nil
}
Task 12 (Hard) — Defer-friendly file writer¶
Write WriteAll(path string, data []byte) (err error) that: - Creates the file. - Writes all the data. - Closes the file. - Returns the first error among Create, Write, and Close.
Solution
func WriteAll(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("create %q: %w", path, err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close %q: %w", path, cerr)
}
}()
if _, werr := f.Write(data); werr != nil {
return fmt.Errorf("write %q: %w", path, werr)
}
return nil
}
Task 13 (Hard) — Custom Is method¶
Define a typed error *HTTPError with Status int. Implement an Is method so that errors.Is(err, target) returns true when target is an *HTTPError with the same Status.
Solution
type HTTPError struct {
Status int
Msg string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("http %d: %s", e.Status, e.Msg)
}
func (e *HTTPError) Is(target error) bool {
t, ok := target.(*HTTPError)
return ok && e.Status == t.Status
}
func main() {
err := &HTTPError{Status: 404, Msg: "not found"}
target := &HTTPError{Status: 404}
fmt.Println(errors.Is(err, target)) // true
}
Task 14 (Hard) — Test-driven error path¶
Write a function LoadConfig(path string) (*Config, error) and a test suite that covers: - Success. - File missing → errors.Is(err, fs.ErrNotExist). - Invalid JSON → errors.As(err, &jsonErr) for *json.SyntaxError.
Implement the function with appropriate wrapping.
Solution
type Config struct {
Name string `json:"name"`
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %q: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse %q: %w", path, err)
}
return &cfg, nil
}
// _test.go
func TestLoadConfig_Missing(t *testing.T) {
_, err := LoadConfig("/nope.json")
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("got %v, want fs.ErrNotExist", err)
}
}
Task 15 (Boss-level) — Build an errors.E constructor¶
Modeled after Upspin's design: E(args ...any) error accepts any combination of: - A string (treated as the operation), - A custom Kind, - An error (treated as cause), - A Path value, - etc.
…and returns a *Error carrying all of them. Implement it.
Solution sketch
type Kind int
const (
KindOther Kind = iota
KindNotFound
KindInvalid
)
type Op string
type Path string
type Error struct {
Op Op
Kind Kind
Path Path
Err error
}
func (e *Error) Error() string { /* compose */ }
func (e *Error) Unwrap() error { return e.Err }
func E(args ...any) error {
e := &Error{}
for _, a := range args {
switch v := a.(type) {
case Op:
e.Op = v
case Kind:
e.Kind = v
case Path:
e.Path = v
case error:
e.Err = v
case string:
e.Op = Op(v)
}
}
return e
}
(See Upspin's errors design for the full pattern.)