errors.New — Tasks¶
Hands-on exercises focused on
errors.New. Each task gives a problem, hints, and a reference solution. Difficulty: easy → hard.
Task 1 (Easy) — Hello, errors.New¶
Write a program that creates an error with the message "hello error" using errors.New, then prints both the error itself and the result of calling Error() on it.
Hints - Import errors and fmt. - fmt.Println(err) calls Error() automatically.
Solution
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("hello error")
fmt.Println(err)
fmt.Println(err.Error())
}
Both lines print hello error.
Task 2 (Easy) — A failing function¶
Write MustBePositive(n int) error that returns nil if n > 0, otherwise returns an error created with errors.New whose message is "n must be positive".
Hints - One if, one errors.New, one nil.
Solution
package main
import (
"errors"
"fmt"
)
func MustBePositive(n int) error {
if n <= 0 {
return errors.New("n must be positive")
}
return nil
}
func main() {
fmt.Println(MustBePositive(5)) // <nil>
fmt.Println(MustBePositive(-3)) // n must be positive
}
Task 3 (Easy) — Declare a sentinel¶
Define a package-level sentinel ErrEmptyName with the message "name cannot be empty". Write Greet(name string) (string, error) that returns the greeting "hello, NAME" if name is non-empty, otherwise the sentinel.
Hints - var ErrEmptyName = errors.New(...) outside any function. - Compare with errors.Is in your test code if you write any.
Solution
package main
import (
"errors"
"fmt"
)
var ErrEmptyName = errors.New("name cannot be empty")
func Greet(name string) (string, error) {
if name == "" {
return "", ErrEmptyName
}
return "hello, " + name, nil
}
func main() {
s, err := Greet("Ada")
fmt.Println(s, err) // hello, Ada <nil>
s, err = Greet("")
fmt.Println(s, err) // (empty) name cannot be empty
if errors.Is(err, ErrEmptyName) {
fmt.Println("matched sentinel")
}
}
Task 4 (Easy) — Pointer-identity demo¶
Write a program that creates two errors with errors.New("same") and prints whether they are equal under ==. Add a comment explaining why.
Solution
package main
import (
"errors"
"fmt"
)
func main() {
a := errors.New("same")
b := errors.New("same")
fmt.Println(a == b) // false
fmt.Println(a.Error() == b.Error()) // true
// Each call to errors.New returns a fresh *errorString pointer.
// The interface compares by dynamic type AND underlying pointer,
// so two separate allocations never compare equal even if
// the message strings are identical.
}
Task 5 (Easy) — errors.Is with a sentinel¶
Given ErrTooLarge = errors.New("too large"), write CheckSize(n int) error that returns ErrTooLarge if n > 100. In main, call it with 200 and use errors.Is to detect the sentinel.
Solution
package main
import (
"errors"
"fmt"
)
var ErrTooLarge = errors.New("too large")
func CheckSize(n int) error {
if n > 100 {
return ErrTooLarge
}
return nil
}
func main() {
err := CheckSize(200)
if errors.Is(err, ErrTooLarge) {
fmt.Println("too large detected")
}
}
Task 6 (Medium) — Wrap a sentinel¶
Define var ErrTimeout = errors.New("timeout"). Write Call() error that always returns ErrTimeout wrapped with the prefix "Call: " using fmt.Errorf and %w. In main, verify errors.Is(err, ErrTimeout) returns true.
Hints - fmt.Errorf("Call: %w", ErrTimeout).
Solution
package main
import (
"errors"
"fmt"
)
var ErrTimeout = errors.New("timeout")
func Call() error {
return fmt.Errorf("Call: %w", ErrTimeout)
}
func main() {
err := Call()
fmt.Println(err) // Call: timeout
fmt.Println(errors.Is(err, ErrTimeout)) // true
}
Task 7 (Medium) — Sentinel set for an in-memory store¶
Build a small in-memory store with Get(key string) (string, error) and Put(key, value string) error. Define sentinels ErrNotFound and ErrExists. Put returns ErrExists if the key is already present. Get returns ErrNotFound if the key is missing.
Hints - Use map[string]string. - Two var Err... = errors.New(...) declarations.
Solution
package main
import (
"errors"
"fmt"
)
var (
ErrNotFound = errors.New("store: not found")
ErrExists = errors.New("store: already exists")
)
type Store struct{ m map[string]string }
func New() *Store { return &Store{m: map[string]string{}} }
func (s *Store) Put(k, v string) error {
if _, ok := s.m[k]; ok {
return ErrExists
}
s.m[k] = v
return nil
}
func (s *Store) Get(k string) (string, error) {
v, ok := s.m[k]
if !ok {
return "", ErrNotFound
}
return v, nil
}
func main() {
s := New()
fmt.Println(s.Put("a", "1"))
fmt.Println(s.Put("a", "2")) // store: already exists
v, err := s.Get("b")
fmt.Println(v, err) // (empty) store: not found
fmt.Println(errors.Is(err, ErrNotFound))
}
Task 8 (Medium) — errors.New vs fmt.Errorf benchmark¶
Write two benchmark functions: one that allocates a fresh errors.New("nope") each iteration, one that returns a package-level sentinel. Run them, compare ns/op and B/op.
Hints - Put benchmarks in a _test.go file. - Use testing.B.
Solution
// file: bench_test.go
package errnew
import (
"errors"
"testing"
)
var ErrNope = errors.New("nope")
func BenchmarkPerCall(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = errors.New("nope")
}
}
func BenchmarkSentinel(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ErrNope
}
}
Run with go test -bench=. -benchmem. Expect ~30 ns/op + 16 B/op for the per-call benchmark, ~0.5 ns/op + 0 B/op for the sentinel benchmark.
Task 9 (Medium) — Parse-or-default with sentinel¶
Define var ErrInvalidPort = errors.New("invalid port"). Write ParsePort(s string) (int, error) that returns the parsed integer if it is in [1, 65535], otherwise the sentinel.
Hints - Use strconv.Atoi. - Combine the parse-error and range-error into one sentinel.
Solution
package main
import (
"errors"
"fmt"
"strconv"
)
var ErrInvalidPort = errors.New("invalid port")
func ParsePort(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil || n < 1 || n > 65535 {
return 0, ErrInvalidPort
}
return n, nil
}
func main() {
for _, s := range []string{"80", "0", "70000", "abc"} {
n, err := ParsePort(s)
if errors.Is(err, ErrInvalidPort) {
fmt.Printf("%q: invalid\n", s)
} else {
fmt.Printf("%q: %d\n", s, n)
}
}
}
Task 10 (Medium) — Hybrid: sentinel + typed error¶
Define ErrValidation = errors.New("validation failed") and a type:
Make *ValidationError implement both error and Is(target error) bool so that errors.Is(err, ErrValidation) returns true for any *ValidationError. Use errors.As to extract the field/reason at the call site.
Solution
package main
import (
"errors"
"fmt"
)
var ErrValidation = errors.New("validation failed")
type ValidationError struct {
Field, Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Reason)
}
func (e *ValidationError) Is(target error) bool {
return target == ErrValidation
}
func validate(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Reason: "must be non-negative"}
}
return nil
}
func main() {
err := validate(-1)
fmt.Println(err) // validation: age: must be non-negative
fmt.Println(errors.Is(err, ErrValidation)) // true
var v *ValidationError
if errors.As(err, &v) {
fmt.Println("field:", v.Field, "reason:", v.Reason)
}
}
Task 11 (Medium) — Detect the per-call mistake¶
Look at this code. What is wrong? Write a corrected version.
func GetItem(id int) (string, error) {
if id == 0 {
return "", errors.New("not found")
}
return "item", nil
}
// elsewhere:
err := someCall()
if err == errors.New("not found") {
// ... handle ...
}
Hints - Two issues: pointer identity, and the comparison style.
Solution
var ErrNotFound = errors.New("not found")
func GetItem(id int) (string, error) {
if id == 0 {
return "", ErrNotFound
}
return "item", nil
}
err := someCall()
if errors.Is(err, ErrNotFound) {
// ... handle ...
}
The two fixes: 1. Declare the sentinel at package scope so the same pointer is returned every time. 2. Use errors.Is (works through wrapping; survives future refactors).
Task 12 (Hard) — Build a typed sentinel registry¶
Build a small "errors registry" pattern: a struct that holds named errors and lets a package register them at init. Use errors.New internally for each registration.
Hints - A map[string]error keyed by the error's identifier. - A Register function called from package init. - A Get function returning the registered error.
Solution
package main
import (
"errors"
"fmt"
)
type Registry struct {
m map[string]error
}
func NewRegistry() *Registry { return &Registry{m: map[string]error{}} }
func (r *Registry) Register(name, msg string) error {
if _, ok := r.m[name]; ok {
panic("registry: duplicate " + name)
}
e := errors.New(msg)
r.m[name] = e
return e
}
func (r *Registry) Get(name string) error { return r.m[name] }
var registry = NewRegistry()
var (
ErrNotFound = registry.Register("not_found", "not found")
ErrExists = registry.Register("exists", "already exists")
)
func main() {
fmt.Println(errors.Is(ErrNotFound, registry.Get("not_found"))) // true
fmt.Println(ErrExists) // already exists
}
This pattern is rarely needed but illustrates the relationship between errors.New and identity: each registered error is a single *errorString with a stable pointer.
Task 13 (Hard) — Replace per-call allocation¶
Rewrite this function to avoid per-call allocation in the success path and the failure path:
func Validate(s string) error {
if s == "" {
return errors.New("empty")
}
if len(s) > 100 {
return errors.New("too long")
}
return nil
}
Solution
package main
import (
"errors"
"fmt"
)
var (
ErrEmpty = errors.New("empty")
ErrTooLong = errors.New("too long")
)
func Validate(s string) error {
switch {
case s == "":
return ErrEmpty
case len(s) > 100:
return ErrTooLong
default:
return nil
}
}
func main() {
fmt.Println(Validate(""))
fmt.Println(Validate("ok"))
}
Now both failure paths return a sentinel (no allocation per call). The success path returns nil (no allocation either).
Task 14 (Hard) — Cross-package sentinel design¶
You have two packages users and accounts, and both want to signal "not found." Without breaking either package's API, design a third shared package and refactor.
Hints - Create errs package with ErrNotFound. - Both packages return the shared sentinel (or wrap it).
Solution
// package users
package users
import (
"fmt"
"myproj/errs"
)
func Get(id int) error {
return fmt.Errorf("users.Get(%d): %w", id, errs.ErrNotFound)
}
// package accounts
package accounts
import (
"fmt"
"myproj/errs"
)
func Get(id int) error {
return fmt.Errorf("accounts.Get(%d): %w", id, errs.ErrNotFound)
}
// caller
err := users.Get(7)
if errors.Is(err, errs.ErrNotFound) { /* 404 */ }
err = accounts.Get(7)
if errors.Is(err, errs.ErrNotFound) { /* 404 */ }
Both packages emit errors that match the same shared sentinel.
Task 15 (Hard) — Don't mutate the sentinel¶
This code has a subtle bug. Find and explain it.
package mypkg
import "errors"
var ErrFoo = errors.New("foo")
func reset() {
ErrFoo = errors.New("foo") // looks innocent, right?
}
Solution
The bug: reset replaces the package-level variable with a brand-new *errorString. Any code holding the old pointer (e.g., a handler that captured ErrFoo at startup) will no longer match errors.Is(oldErr, ErrFoo) because ErrFoo now points to a different allocation.
Fix: never reassign sentinels. Treat them as const. Remove the reset function.
If a real test really needs to exercise some kind of "reset," refactor the package to inject the error rather than reassign a global.
Task 16 (Hard) — Custom Is to broaden a sentinel¶
Define ErrTransient = errors.New("transient failure"). Then define a typed error *NetworkError that wraps a cause and reports as transient via Is. Write a retry helper that retries up to 3 times only when errors.Is(err, ErrTransient).
Solution
package main
import (
"errors"
"fmt"
)
var ErrTransient = errors.New("transient failure")
type NetworkError struct{ Cause error }
func (e *NetworkError) Error() string { return "network: " + e.Cause.Error() }
func (e *NetworkError) Is(target error) bool {
return target == ErrTransient
}
func (e *NetworkError) Unwrap() error { return e.Cause }
func retry(fn func() error, n int) error {
var err error
for i := 0; i < n; i++ {
err = fn()
if err == nil {
return nil
}
if !errors.Is(err, ErrTransient) {
return err
}
}
return fmt.Errorf("retry: gave up after %d attempts: %w", n, err)
}
func flaky() error { return &NetworkError{Cause: errors.New("connection reset")} }
func main() {
err := retry(flaky, 3)
fmt.Println(err)
fmt.Println(errors.Is(err, ErrTransient)) // true
}
The *NetworkError reports itself as ErrTransient-equivalent without literally wrapping the sentinel. The retry helper makes its decision via errors.Is.