Don't panic!

September 20, 2024

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 and recover, 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:

# Don’t panic!

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)


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe