I'm sure someone out there has already thought of this. But I stumbled on it when looking at
errors.As
statements in a contribution I was making to Flipt.UPDATE: I found an existing proposal to add this to the standard library and in there it appears this was called out in the generic design proposal itself: https://github.com/golang/go/issues/51945.
TL;DR
This function is handy for brevity and to avoid a potential runtime panic:
func As[E error](err error) (e E, _ bool) {
return e, errors.As(err, &e)
}
errors.As
If you've ever used the standard-libraries
errors.As
function, you might be familiar with its quirks.Firstly, you're going to have to explicitly declare a variable and its type on one line.
Only then can you create a pointer to that variable and pass it to
errors.As
.package main
import "errors"
type MyError string
func (e MyError) Error() string {
return string(e)
}
func returnsMyError() error {
return fmt.Errorf("wrapped my error: %w", MyError("my error"))
}
func main() {
var merr MyError
if errors.As(returnsMyError(), &merr) {
fmt.Println("The root of the error was a MyError type:", merr)
}
}
The program above prints:
The root of the error was a MyError type: my error
Note that in
main()
we first define the variable and type we're interested in.
Then on the subsequent line we create a pointer to this variable and pass it to errors.As
.The ergonomics are not great here.
However, there are also hazards if your custom error is implemented on a pointer to a type.
type MyError struct {
Message string
}
func (e *MyError) Error() string {
return e.String()
}
In this situation it can be easy when calling
errors.As
to miss the fact that you need to pass it a **MyError
(Congrats, you're one step closer to becoming a three-star programmer).
Leading to accidents like the following.// ...
func returnsMyError() error {
return &MyError{Message: "my error"}
}
func main() {
var merr MyError
if errors.As(returnsMyError(), &merr) {
fmt.Println("The root of the error was a MyError type:", merr)
}
}
Which leads to a runtime panic:
panic: errors: *target must be interface or implement error
goroutine 1 [running]:
errors.As({0x4bb008, 0xc000094230}, {0x48d200, 0xc000094220?})
/go/src/errors/wrap.go:89 +0x3df
You must be sure to pass a pointer to an error implementation.
func main() {
var merr *MyError // *MyError is the error implementation not MyError
if errors.As(returnsMyError(), &merr) {
fmt.Println("The root of the error was a MyError type:", merr)
}
}
Solution
Generics (introduced in Go 1.18) provide a handy way to one-line
errors.As
and it provides compile-time safety to avoid this panic.All you need is this handful of lines:
import "errors"
// ...
func As[E error](err error) (e E, _ bool) {
return e, errors.As(err, &e)
}
With this you can one-line the declaration and assertion like so:
// ...
func main() {
if merr, match := As[MyError](returnsMyError()); match {
fmt.Println("The root of the error was a MyError type:", merr)
}
}
- No more pesky variable declaration required before the conditional.
- If your type implements
Error()
over a pointer you can't mistakenly attempt to callAs
with the non-pointer type.
See the what the following attempt to make the mistake in (2) does:
package main
import (
"errors"
"fmt"
)
func As[E error](err error) (e E, _ bool) {
return e, errors.As(err, &e)
}
type MyError string
// Error() is implemented over a *MyError (pointer to MyError)
func (e *MyError) Error() string {
return string(*e)
}
func returnsMyErrorPointer() error {
err := MyError("my errors")
return &err
}
func main() {
if merr, match := As[MyError](returnsMyErrorPointer()); match {
fmt.Println("The root of the error was a MyError type:", merr)
}
}
This program actually fails at compile time with:
./prog.go:24:23: MyError does not implement error (Error method has pointer receiver)
Removing the runtime panic situation altogether.
Final Thoughts
This is not a perfect solution. There still exists one particular trap.
It is a trap that exists in the standard library
errors.As
too.When you don't accept a pointer to a type when implementing
Error()
then the compiler will accept that *YourErrorType
implements error
still.
Because a method on non-pointer type can be called on a pointer to it.
The pointer is just dereferenced before invoking the function (Error()
in this case).
So one final subtle easy to make mistake remains.type MyError string
func (e MyError) Error() string {
return string(e)
}
func returnsMyErrorPointer() error {
err := MyError("my errors")
return &err
}
func main() {
if merr, match := As[MyError](returnsMyErrorPointer()); match {
fmt.Println("The root of the error was a MyError type:", merr)
return
}
fmt.Println("Does not match")
}
This compiles and prints
Does not match
. Because it is *MyError
which is returned, and not MyError
. This is an easy mistake to make. Sadly I think this is unavoidable.