We’re ready for the section of the Go spec that talks about panic
and recover
, Go’s rough analogs to throw
and catch
.
Handling panics
Two built-in functions,
panic
andrecover
, assist in reporting and handling run-time panics and program-defined error conditions.func panic(interface{}) func recover() interface{}
But before we dive into a discussion of how these things work, and how to use them, I want to offer some caution, in the form of one of the famous Go Proverbs:
See Effective Go: Errors. Don’t use panic for normal error handling. Use error and multiple return values.
The temptation to panic
can be strong, especially if you’ve come from a language that uses the throw
/try
/catch
paradigm for error handling. Please. PLEASE! Don’t do this in Go.
panic
should be reserved for truly exceptional cases, or conditions that should be impossible. Things like: reaching the end of an infinite loop, or attempting to divide by zero.
When deciding “Should I return an error, or panic?” you should almost always prefer returning an error. Cases when panic
might be an acceptable alternative:
- Conditions that are impossible. If you reach the end of an infinte loop, then something is wrong with your code (i.e. someone edited the code in an unexpected way), so a
panic
might be appropriate. - Conditions that indicate a programmer error, not a user error. This is a special case of the first; I’ll sometimes use a
panic
when I receive an invalid function argument, which I know represents a programming error, not a user input error. The theory is that a panic will be more noticable to the developers, and really should never happen anyway, except in the case of a serious programming bug. - When parsing a known good value fails. Yet another example of an impossible condition. For example:
var x = regexp.MustParse(`\d`)
(regexp.MustParse
will panic on error)
I’ll also mention one other case that you should “never” do. Except maybe when you choose to.
Some libraries will use panic
as a shortcut for tedious error handling. This is most common in libraries that do a lot of I/O. As an example, let’s imagine we want to print a string as JSON to an io.Writer
func marshalString(w io.Writer, s string) error {
_, err := io.Write([]byte{'"'})
if err != nil {
return err
}
_, err = io.Write([]byte(s))
if err != nil {
return err
}
_, err = io.Write([]byte{'"'})
if err != nil {
return err
}
return nil
}
This is just a simple string function, imagine doing the same thing recursively for arrays and maps. All that error checking is indeed tedious, and makes the code rather hard to read.
Maybe you want to simplify this with a panic helper:
panicIf(err error) {
if err != nil {
panic(err)
}
}
Now that annoying code can be made simpler:
func marshalString(w io.Writer, s string) error {
_, err := io.Write([]byte{'"'})
panicIf(err)
_, err = io.Write([]byte(s))
panicIf(err)
_, err = io.Write([]byte{'"'})
panicIf(err)
}
Or maybe you go one step further, and wrap io.Write
:
func mustWrite(w io.Writer, b []byte) {
if _, err := w.Write(b); err != nil {
panic(err)
}
}
func marshalString(w io.Writer, s string) error {
panicWrite(w, []byte{'"'})
panicWrite(w, []byte(s))
panicWrite(w, []byte{'"'})
}
In either case, you still need something to recover
from those panics, so that your program doesn’t just crash.
And this brings me to the key point, if you ever choose to do this: Don’t let the panics cross your API boundary!
In other words, if you choose the risky path of using panic
for error handling, recover from all of your own panics.
Want an example of this being done in the wild? Take a look at the encoding/json
package in the standard library. It does this.
Quotes from The Go Programming Language Specification Language version go1.23 (June 13, 2024)