...
If the client needs to detect the error, and you have created a simple error using errors.New
, use a var for the error.
Bad | Good |
---|
Code Block |
---|
| // package foo
func Open() error {
return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
panic("unknown error")
}
}
|
| Code Block |
---|
| // package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// handle
} else {
return errors.Wrap(err, "unknown error")
}
} |
|
If you have an error that clients may need to detect, and you would like to add more information to it (e.g., it is not a static string), then you should use a custom type.
Bad | Good |
---|
Code Block |
---|
| func open(file string) error {
return fmt.Errorf("file %q not found", file)
}
func use() error {
if err := open(); err != nil {
if strings.Contains(err.Error(), "not found") {
// handle
} else {
panic("unknown error")
}
}
} |
| Code Block |
---|
| type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func open(file string) error {
return errNotFound{file: file}
}
func use() error {
if err := open(); err != nil {
var nfErr errNotFound
if errors.As(err, &nfErr) |
|
; ok {
// handle
} else {
return errors.Wrap(err, "opening file")
}
}
} |
|
Error Wrapping
There are three main options for propagating errors if a call fails:
...
When adding context to returned errors, keep the context succinct by avoiding phrases like "failed to", which state the obvious and pile up as the error percolates up through the stack:
Bad | Good |
---|
Code Block |
---|
| s, err := store.New()
if err != nil {
return errors.Newf(
"failed to create new store: %s", err)
} |
| Code Block |
---|
| s, err := store.New()
if err != nil {
return errors.Wrap(
"new store", err)
} |
|
Code Block |
---|
failed to x: failed to y: failed to create new store: the error
|
| Code Block |
---|
x: y: new store: the error
|
|
However once the error is sent to another system, it should be clear the message is an error (e.g. an err
tag or "Failed" prefix in logs).
...
The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|
Code Block |
---|
| t := i.(string) |
| Code Block |
---|
| t, ok := i.(string)
if !ok {
// handle the error gracefully
} |
|
Don't Panic
Code running in production must avoid panics. Panics are a major source of cascading failures. If an error occurs, the function must return an error and allow the caller to decide how to handle it.
Bad | Good |
---|
Code Block |
---|
| func foo(bar string) {
if len(bar) == 0 {
panic("bar must not be empty")
}
// ...
}
func main() {
if len(os.Args) != 2 {
fmt.Println("USAGE: foo <bar>")
os.Exit(1)
}
foo(os.Args[1])
} |
| Code Block |
---|
| func foo(bar string) error {
if len(bar) == 0
return errors.New("bar must not be empty")
}
// ...
return nil
}
func main() {
if len(os.Args) != 2 {
fmt.Println("USAGE: foo <bar>")
os.Exit(1)
}
if err := foo(os.Args[1]); err != nil {
panic(err)
}
} |
|
Panic/recover is, in most cases, not an error handling strategy. A program must panic only when something irrecoverable happens such as a nil dereference.
...
Even in tests, prefer t.Fatal
or t.FailNow
over panics to ensure that the test is marked as failed.
Bad | Good |
---|
Code Block |
---|
| // func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
} |
| Code Block |
---|
| // func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
} |
|