When should you panic instead of returning an error?
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
| Situation | Choice |
|---|---|
| A file might not exist | error |
| A network call might fail | error |
| User input might be invalid | error |
| An index you computed yourself goes out of bounds | Bug — let the runtime panic surface it |
| A required config value that should always be set by the time this code runs is nil | panic, deliberately, to fail fast |