Advanced errors
| Wrap with %w | |
| errors.Is and As | |
| Sentinel errors | |
| panic and recover boundaries |
Wrap with %w
The Errorf function with the %w verb attaches the original error to a message with context. Callers can still print the full chain, and matching helpers can look through the wrapping to find the cause.
package main
import (
"fmt"
"os"
)
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
return data, nil
}
func main() {
if _, err := readConfig("missing.txt"); err != nil {
fmt.Println(err)
}
}
Each layer adds what it knows, such as the file it wanted, while %w preserves what happened below. Unwrap the chain with errors.Unwrap when you need the raw cause for logging.
errors.Is and As
Never compare wrapped errors with double equals: wrapping hides the inner value from direct comparison. The errors.Is call walks the chain looking for a target, while errors.As pulls out a value of a chosen type so you can inspect its fields.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
_, err := os.ReadFile("missing.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file does not exist")
return
}
if err != nil {
fmt.Println(err)
}
}
The same idea with types:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
_, err := os.ReadFile("missing.txt")
var perr *os.PathError
if errors.As(err, &perr) {
fmt.Println("path:", perr.Path)
}
}
Is answers whether the chain contains a known value, and As hands you the matching error with its fields attached. Choose Is for decisions and As for details.
Sentinel errors
A sentinel is a package-level error value that names one expected failure. Return it directly, document it beside the function, and let callers match it with errors.Is even through wrapping.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("user not found")
func Find(users map[string]int, name string) (int, error) {
id, ok := users[name]
if !ok {
return 0, ErrNotFound
}
return id, nil
}
func main() {
users := map[string]int{"ann": 7}
if _, err := Find(users, "bob"); errors.Is(err, ErrNotFound) {
fmt.Println("unknown user")
}
}
Callers test for ErrNotFound instead of parsing message text, so wording can improve without breaking anyone. Keep the sentinel set small: a handful of documented failures beats a catalog nobody memorizes.
panic and recover boundaries
Panic is for broken promises, not for ordinary failures: out-of-range indexes and nil maps signal programmer mistakes. Recover belongs at the boundary, inside a deferred function, where a library turns a crash into an error and the program keeps running.
package main
import "fmt"
func calm() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}
func risky() {
defer calm()
panic("boom")
}
func main() {
risky()
fmt.Println("program continues")
}
The deferred calm runs while the panic unwinds risky, and recover stops the crash with the value in hand. Use this pattern at package borders, and let ordinary errors travel home as return values.
Next: defer
Article author: Arthur Isaev