Panic
The panic built-in and when the runtime panics - programmer errors vs expected failures and the difference from exceptions.
Panic is Go's mechanism for signaling an unrecoverable error. It stops the normal execution flow, defers any deferred functions (with LIFO order), and crashes the program - unless a defer recovers it. Panic is for programmer errors (conditions that should never occur in a correctly written program), not for expected failures (use error returns instead).
Call panic to halt execution immediately. All deferred functions in the current goroutine run before the panic propagates. A panic that reaches the main goroutine crashes the entire program with a stack trace.
package main
import (
"fmt"
)
func main() {
defer fmt.Println("deferred 1")
defer fmt.Println("deferred 2")
fmt.Println("before panic")
panic("something went wrong")
fmt.Println("never reached")
}
// Output:
// before panic
// deferred 2
// deferred 1
// panic: something went wrongCommon runtime panics: nil pointer dereference, out-of-bounds array/slice access, type assertion on a wrong type, division by zero, closing a closed channel, or sending on a closed channel.
package main
import (
"fmt"
)
func main() {
// Nil pointer dereference
var p *int
// fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
// Out of bounds
s := []int{1, 2, 3}
// fmt.Println(s[10]) // panic: runtime error: index out of range [10] with length 3
// Type assertion failure (with comma-ok, no panic)
var x interface{} = "hello"
n, ok := x.(int)
if !ok {
fmt.Println("not an int:", n, ok) // 0 false - no panic
}
// Type assertion without comma-ok panics
// n := x.(int) // panic: interface conversion: interface {} is string, not int
}When to panic vs return an error: panic is for unrecoverable programmer errors (preconditions violated, invariants broken). Expected failures that callers might recover from (invalid input, network timeout, resource not found) should be returned as errors, not panics.
package main
import (
"errors"
"fmt"
)
// ParseInt panics on invalid input (programmer error - caller should validate).
// A real parser would return an error.
func ParseInt(s string) int {
for i, c := range s {
if c < '0' || c > '9' {
panic(fmt.Sprintf("ParseInt: invalid char %q at index %d", c, i))
}
}
// simplified; real code would actually parse
return 0
}
// WaitForResponse returns an error on timeout (expected failure - caller handles it).
func WaitForResponse(timeoutMs int) (string, error) {
if timeoutMs < 0 {
return "", errors.New("timeout must be non-negative")
}
// simplified; real code would wait
return "ok", nil
}
func main() {
resp, err := WaitForResponse(100)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("response:", resp)
}
// Panics are for things that should never happen
// result := ParseInt("invalid") // panic
}In production
Panics in a goroutine that is not recovered will crash the entire process - a single unhandled panic cascades to shutdown. In a web service, a handler panic should be caught at the HTTP framework boundary, converted to a 500 response with logging, and the server should continue serving. Never let a panic from user input or external data escape uncaught. Use panics only for invariant violations that indicate serious bugs (e.g., a type assertion that is supposed to be guaranteed by the calling code, or a struct state that should be impossible). For all expected failures, return an error and let callers decide whether to propagate, retry, or panic.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free