Go > Error Handling > Panic and Recover > Using panic

Panic and Recover: Safe Division

This code snippet demonstrates how to use `panic` and `recover` to handle errors during division, preventing program crashes and providing a graceful recovery mechanism. It illustrates a common use case for `panic` when encountering unexpected or unrecoverable situations.

The core concept: Safe Division with Panic and Recover

This example defines a function `safeDivide` that performs division. If the denominator is zero, it calls `panic` with an error message. The `main` function uses `defer` and `recover` to catch any panics that occur during the execution of `safeDivide`. When a panic occurs, `recover` captures the panic value, logs it, and allows the program to continue execution from the point after the `panic` call within `safeDivide`. Critically, if `recover` isn't called in a `defer`red function higher up the call stack, the program crashes.

package main

import (
	"fmt"
	"log"
)

func safeDivide(numerator, denominator int) int {
	if denominator == 0 {
		panic("division by zero")
	}
	return numerator / denominator
}

func main() {
	defer func() {
		if r := recover(); r != nil {
			log.Println("Recovered from panic:", r)
			// Optionally, take corrective action here.
			fmt.Println("Program continues after error.")
		}
	}()

	result := safeDivide(10, 2)
	fmt.Println("Result:", result)

	// This will cause a panic
	result = safeDivide(5, 0)
	fmt.Println("Result:", result) // This line will not be executed

	fmt.Println("Program continues...") // This line will be executed because of recover
}

Explanation of the Code

The `safeDivide` function checks for division by zero. If the denominator is zero, it calls `panic`. The `panic` function immediately stops the normal execution of the function, unwinds the stack, and executes any deferred functions. The `defer` statement in `main` registers a function that will be executed when `main` exits, either normally or due to a panic. Inside the deferred function, `recover` is called. If a panic occurred, `recover` returns the value passed to `panic`. If no panic occurred, `recover` returns `nil`. By checking the return value of `recover`, we can determine whether a panic occurred and take appropriate action. In this case, we log the error message and print a message indicating that the program continues after the error.

Concepts Behind the Snippet

panic is a built-in function that stops the normal execution of the current goroutine. When a function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the executing goroutine have returned, at which point the program crashes. However, panic can be intercepted by using the built-in recover function. recover stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. It's important to note that recover only works effectively when called within a deferred function.

Real-Life Use Case Section

A real-life use case for panic and recover is within a web server. Consider a handler that processes user requests. If a request triggers a critical error that would otherwise crash the server (e.g., accessing a resource that is unexpectedly unavailable), panic can be used to signal the error. A higher-level recovery mechanism (often in middleware) can catch the panic, log the error, and return an appropriate error response to the user, preventing the entire server from crashing. Another example could be in a critical data processing pipeline where an unexpected data format can corrupt the entire pipeline. Panicking and recovering can isolate the corrupt data and allow the pipeline to continue processing other data.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func riskyOperation() {
	panic("Something went terribly wrong!")
}

func handler(w http.ResponseWriter, r *http.Request) {
	defer func() {
		if err := recover(); err != nil {
			log.Println("Recovered panic:", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}
	}()

	riskyOperation()

	fmt.Fprintln(w, "Request processed successfully.")
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Best Practices

  • Use panic sparingly: Prefer returning errors using the error interface for recoverable errors. Use panic only for truly exceptional situations that should never occur under normal circumstances.
  • recover only at the top level: It's generally best to recover only in the top-level function of a goroutine (e.g., the main function or the entry point of a worker goroutine). This prevents unexpected side effects from recovering in deeply nested functions.
  • Log panics: Always log the details of a panic so that you can diagnose the cause.
  • Provide context in panic messages: Include enough information in the panic message to help identify the source of the error.
  • Consider using named return values: For improved clarity when using recover, use named return values to easily set a proper result from the deferred function.

Interview Tip

Be prepared to explain the difference between errors and panics. Errors are used to indicate recoverable conditions, while panics are used to indicate unrecoverable conditions. You should also be able to describe how defer and recover work together to handle panics gracefully. Be prepared to discuss scenarios where panic/recover is a suitable solution.

When to use them

Use panic when a program encounters a situation it cannot reasonably recover from. Examples include:

  • Unexpected nil pointers.
  • Data corruption.
  • Violations of internal invariants that should never happen.
  • Initialization failures during program startup.
Use recover to prevent the entire program from crashing when a panic occurs, particularly in long-running services or applications.

Memory footprint

The memory footprint of panic and recover is generally small. When a panic occurs, the Go runtime unwinds the stack, executing deferred functions along the way. The memory overhead is mainly due to the deferred function calls and the storage of the panic value. However, excessive use of panic and recover can potentially impact performance, especially if panics occur frequently, as stack unwinding is a relatively expensive operation compared to normal error handling.

Alternatives

The primary alternative to using panic and recover is to use the standard error handling mechanism (returning error values). For most situations, returning errors is the preferred approach. Consider using custom error types to provide more context about the error.

Pros

  • Can prevent program crashes in exceptional situations.
  • Provides a way to handle unrecoverable errors gracefully.
  • Can simplify error handling in certain cases where deeply nested functions might encounter errors.

Cons

  • Can make code harder to reason about if used excessively.
  • Stack unwinding during a panic can be relatively expensive.
  • Hides errors if not handled correctly (i.e., if recover is not used).
  • May not be suitable for all types of error handling, especially recoverable errors.

FAQ

  • What happens if I don't call `recover` after a `panic`?

    If you don't call `recover` in a deferred function, the panic will continue to propagate up the call stack until it reaches the top-level function of the goroutine. At that point, the program will terminate and print a stack trace.
  • Can I recover from a panic in a different goroutine?

    No, `recover` can only catch panics that occur in the same goroutine. Panics do not cross goroutine boundaries.
  • Is it good practice to use panic/recover for all error handling?

    No, it's generally not a good practice. Use errors for normal, recoverable conditions, and reserve `panic` for truly exceptional situations that should never occur. Overuse of panic/recover can make code harder to understand and debug.