The Secret Life of Go: Panic and Recover
Source: Dev.to
Chapter 21: The Emergency Brake
“I think I killed it,” Ethan whispered.
He was staring at his terminal. His web server, which had been running smoothly for weeks, was gone. No logs, no shutdown message—just a sudden return to the command prompt.
“What did you do?” Eleanor asked, leaning over his shoulder.
“I just sent a POST request with an empty JSON body,” Ethan said. “I expected a 400 Bad Request error. Instead, the whole server vanished.”
“Show me the logs,” Eleanor said.
Ethan scrolled up.
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a2b40]
goroutine 1 [running]:
main.HandleRequest(...)
/server/main.go:42 +0x45
main.main()
/server/main.go:80 +0x120
“Ah,” Eleanor nodded. “You are trying to access a field on a
nilpointer. In C++ this would be a segmentation fault. In Go it is a panic.”
“But I check for errors everywhere!” Ethan protested.
“A panic is not an error,” Eleanor corrected. “An error is a polite knock at the door: ‘Excuse me, I couldn’t open this file.’ A panic is a fire alarm: ‘EVERYTHING IS BURNING.’ It stops execution immediately, runs your deferred functions, and then crashes the program.”
The Unwinding
“So… game over?” Ethan asked.
“Not necessarily,” Eleanor said. “When a panic happens, it bubbles up through the stack, function by function. It’s like an elevator free‑falling down a shaft.”
She sketched a diagram:
HandleRequest panics! → CRASH
ServeHTTP stops → CRASH
main stops → EXIT PROGRAM
“We need emergency brakes,” Eleanor said. “We need a mechanism to stop the free‑fall before it hits the bottom and kills the process.”
Containing the Blast (Recover)
“In Chapter 20 we learned about
defer,” she continued. “That is the only place where you can stop a panic.”
“Why only there?”
“Because when a function is panicking it skips all normal code. It only runs deferred functions. If you want to catch the panic, you have to be waiting in the defer stack.”
She opened a new file to write a middleware:
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// The safety net caught something!
log.Printf("Panic recovered: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
“The
recover()function is magic,” Eleanor explained. “If the program is panicking,recover()catches the panic value and stops the crashing elevator. It returns control to you. If the program is not panicking, it just returnsniland does nothing.”
“So it converts a crash into a controlled landing?”
“Exactly. It contains the blast radius. One bad request dies, but the server lives on.”
Ethan updated his main function:
func main() {
handler := http.HandlerFunc(HandleRequest)
safeHandler := RecoveryMiddleware(handler) // Wrap it in the safety net
http.ListenAndServe(":8080", safeHandler)
}
He ran the server and sent the “poison” request again.
- Terminal:
Panic recovered: runtime error: invalid memory address or nil pointer dereference - Server status: Still running.
“It didn’t crash!” Ethan cheered.
The Rule of Law
“Now,” Eleanor said, her voice dropping to a serious tone, “you know how to use
panicandrecover. My advice to you is: Don’t.”
Ethan blinked. “What?”
“Do not use
panicfor normal error handling,” she warned. “If a user file is missing, return anerror. If the database is down, return anerror.”
“So when do I use it?”
“Only for the impossible,” Eleanor said. “Use it when the programmer has made a mistake so severe that the code cannot logically continue. Or during startup, if a required configuration is missing.”
func MustLoadConfig() *Config {
cfg, err := load("config.yaml")
if err != nil {
panic("cannot load config: " + err.Error())
// Acceptable: the app literally cannot start without this.
}
return cfg
}
“We use
recoverat the boundary of our system—like in this middleware—to prevent a single buggy request from taking down the whole ship. But inside your business logic? Stick to errors.”
She added one final warning. “Remember: recover only works in the same goroutine. If you spawn a background worker and it panics, this middleware won’t save you. The whole program will crash.”
Ethan looked at his code. “So… panic is for crashes, errors are for problems.”
“Precisely,” Eleanor smiled. “And
recoveris the parachute that ensures you live to debug another day.”
Key Concepts from Chapter 21
Panic
- A built‑in function that stops the ordinary flow of control.
- Effect: Stops the current function, runs any
deferstatements, then crashes the program (unless recovered). - Cause: Runtime errors (nil‑pointer dereference, index out of range) or explicit
panic()calls.
Recover
- A built‑in function that regains control of a panicking goroutine.
- Requirement: Must be called inside a deferred function.
- Behavior:
- If called during a panic, it captures the panic value and stops the crash.
- If called normally, it returns
nil.
The “Must” Pattern
- Idiomatic in Go to panic during initialization if a required resource fails (e.g.,
regexp.MustCompile, loading config). - If the app can’t run, it should crash early.
Goroutine Boundary
recoveronly works in the same goroutine where the panic occurred. Panics in other goroutines will terminate the program unless they are recovered within that goroutine.
Key Points
- A
paniconly bubbles up the stack of the current goroutine. - If you spawn a generic
go func(), and it panics, arecoverinmainwill not catch it. The program will crash.
Next chapter: The Context Package. Ethan learns how to cancel a request that is taking too long.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.