When should you panic instead of returning an error?

5 minintermediategopanicerror-handlingdesign

Quick Answer

Panic when a failure represents a programming bug or a violated invariant that should be impossible in correct code — an out-of-bounds index, a nil pointer dereference, or a required argument a function's contract says must never be missing. Return an error for anything a caller can reasonably expect to happen and might want to handle gracefully — a missing file, a failed network call, invalid user input. A useful test: if the failure is something the caller could meaningfully recover from or retry, it should be an error; if it's something only a fix to the code itself could prevent, panic is more honest, since it surfaces the bug loudly instead of letting a caller silently ignore an error return.

Detailed Answer

The line between "return an error" and "panic" comes down to one question: could a correct, well-behaved caller reasonably trigger this?

Error: expected, recoverable conditions

func OpenConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("reading config: %w", err)
    }
    // ...
}

A missing config file is completely plausible in normal operation — a user typo'd a path, a deploy didn't include the file yet. The caller should get a chance to log it, retry, or fall back to defaults.

Panic: a violated invariant, a real bug

func MustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(err)   // a hardcoded, "known good" pattern that fails to compile is a bug
    }
    return re
}

The standard library's own regexp.MustCompile, template.Must, and similar Must* functions follow this exact pattern: they're meant to be called with values that are hardcoded and known correct at compile time, so a failure indicates a programmer mistake, not a runtime condition a caller should handle.

The Must-prefix convention

var validEmailPattern = regexp.MustCompile(`^[^@]+@[^@]+$`)  // panics at package init if the pattern is wrong — good, catch it immediately

Using Must* at package-level initialization is a common, accepted pattern: if the hardcoded input is wrong, you want the program to fail immediately and loudly at startup, not limp along with a broken regex.

A simple decision table

SituationChoice
A file might not existerror
A network call might failerror
User input might be invaliderror
An index you computed yourself goes out of boundsBug — let the runtime panic surface it
A required config value that should always be set by the time this code runs is nilpanic, deliberately, to fail fast

Related Resources