36. Custom error types
A sentinel says what went wrong. A custom error type also says how much,
which field, what status code — anything the caller needs in order to
react intelligently. Since error is just an interface, making one is a
struct and a method.
Defining one
package main
import "fmt"
type ValidationError struct {
Field string
Value string
Rule string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("field %q with value %q violates rule %q", e.Field, e.Value, e.Rule)
}
func validateAge(value string) error {
if value == "" {
return &ValidationError{Field: "age", Value: value, Rule: "required"}
}
if value == "abc" {
return &ValidationError{Field: "age", Value: value, Rule: "numeric"}
}
return nil
}
func main() {
fmt.Println(validateAge(""))
fmt.Println(validateAge("abc"))
fmt.Println(validateAge("36"))
}
Three things to copy from that snippet:
- The type name ends in
Error(ValidationError,ParseError,HTTPError) — that's the convention, mirroringErr...for sentinels. - The method has a pointer receiver, and you return
&Value{...}. Pointer errors compare by identity, so two distinct failures never accidentally look equal, and there's no copying. Error()produces a lowercase, punctuation-free sentence, exactly like a sentinel's message.
Getting the data back out
The caller recovers the concrete type with a type assertion:
package main
import "fmt"
type HTTPError struct {
Code int
URL string
Retry bool
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("%s returned %d", e.URL, e.Code)
}
func get(url string) error {
switch url {
case "/ok":
return nil
case "/missing":
return &HTTPError{Code: 404, URL: url, Retry: false}
default:
return &HTTPError{Code: 503, URL: url, Retry: true}
}
}
func main() {
for _, url := range []string{"/ok", "/missing", "/flaky"} {
err := get(url)
if err == nil {
fmt.Println(url, "-> fine")
continue
}
if he, ok := err.(*HTTPError); ok {
if he.Retry {
fmt.Printf("%s -> %d, will retry\n", url, he.Code)
} else {
fmt.Printf("%s -> %d, giving up\n", url, he.Code)
}
continue
}
fmt.Println(url, "-> unknown error:", err)
}
}
he.Retry is the payload — a decision the producer of the error is best
placed to make, handed to the consumer who has to act on it. A sentinel
could never carry that.
errors.As — the assertion that works through wrapping
The direct assertion above breaks the moment somebody wraps the error, for
exactly the reason == broke in the last lesson. The standard fix is
errors.As:
var he *HTTPError
if errors.As(err, &he) {
fmt.Println("status was", he.Code)
}
Read it as the type-aware sibling of errors.Is:
| you want to know | you call |
|---|---|
| is this that specific error value? | errors.Is(err, ErrNotFound) |
| is this that kind of error, and give me it | errors.As(err, &target) |
Both walk the %w chain. errors.As takes a pointer to a variable of the
error type you want — &he, where he is a *HTTPError — and fills it
in if it finds a match anywhere in the chain. Passing anything else panics,
so read that line carefully: it's & plus a variable of the pointer type.
The full pattern in real code:
func handle(err error) {
var he *HTTPError
var ve *ValidationError
switch {
case err == nil:
return
case errors.As(err, &he):
log.Printf("http %d from %s", he.Code, he.URL)
case errors.As(err, &ve):
log.Printf("bad field %s", ve.Field)
case errors.Is(err, ErrTimeout):
log.Print("timed out")
default:
log.Printf("unexpected: %v", err)
}
}
(Those two boxes are reference-only. errors.As reaches into the runtime
type system in a way the in-browser interpreter can't reproduce for types
you define in the box — it works perfectly in a compiled Go program. The
runnable examples on this page use the direct assertion instead, which
behaves the same when the error hasn't been wrapped.)
Making your type work with errors.Is
If your custom type wraps another error, give it an Unwrap method and the
whole chain keeps working:
package main
import (
"errors"
"fmt"
)
var ErrTimeout = errors.New("timeout")
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error {
return e.Err
}
func run(q string) error {
return &QueryError{Query: q, Err: ErrTimeout}
}
func main() {
err := run("SELECT * FROM users")
fmt.Println(err)
if qe, ok := err.(*QueryError); ok {
fmt.Println("the failing query was:", qe.Query)
fmt.Println("caused by:", qe.Err)
fmt.Println("is timeout:", errors.Is(qe.Err, ErrTimeout))
}
}
Unwrap() error is the one method that makes your type a good citizen: with
it, errors.Is(err, ErrTimeout) finds the sentinel through your struct,
and errors.Unwrap peels it. It's the same interface fmt.Errorf's %w
produces internally — nothing magic.
An Err error field plus an Unwrap method is the standard shape for
"my error, wrapping theirs".
The nil-pointer trap, again
This one is important enough to repeat from the interfaces module, because custom error types are exactly where it bites:
func doWork() error {
var e *MyError // a nil *MyError
if failed {
e = &MyError{...}
}
return e // BUG: never nil as an `error`
}
Returning a typed nil pointer gives the caller a non-nil error holding a
nil pointer, so if err != nil is always true. Declare the function as
returning error, and return a literal nil:
func doWork() error {
if failed {
return &MyError{...}
}
return nil
}
Sentinel or custom type?
| use a sentinel when | use a custom type when |
|---|---|
| the caller only needs to know which failure | the caller needs details (field, code, retry-after) |
| there's a small fixed set of cases | the error carries variable data |
| you want the simplest possible API | you're wrapping another error with structure |
Start with a sentinel. Promote to a type when you catch yourself parsing the message string to get information out — that's the signal that the data should have been a field.
Your turn
Define RangeError with Min, Max and Got fields and an Error()
method, return it from check, and pull Got back out with an assertion:
value 42 is outside 1..10
the offending value was 42
package main
import "fmt"
// define RangeError (pointer receiver on Error) here
func check(n int) error {
if n < 1 || n > 10 {
// return a *RangeError
}
return nil
}
func main() {
err := check(42)
fmt.Println(err)
if re, ok := err.(*RangeError); ok {
fmt.Println("the offending value was", re.Got)
}
}
package main
import "fmt"
type RangeError struct {
Min int
Max int
Got int
}
func (e *RangeError) Error() string {
return fmt.Sprintf("value %d is outside %d..%d", e.Got, e.Min, e.Max)
}
func check(n int) error {
if n < 1 || n > 10 {
return &RangeError{Min: 1, Max: 10, Got: n}
}
return nil
}
func main() {
err := check(42)
fmt.Println(err)
if re, ok := err.(*RangeError); ok {
fmt.Println("the offending value was", re.Got)
}
}
Next: the failures that aren't errors — panic and recover.