When writing Go applications, developers often need to handle catastrophic errors that prevent the program from continuing. Go provides two primary mechanisms for handling such situations: log.Fatalf()
and panic
. While both can terminate program execution, they serve different purposes and come with their own sets of trade-offs.
Understanding log.Fatalf()
log.Fatalf()
is part of Go’s standard logging package and provides a way to log a message before terminating the program. When called, it:
- Formats and writes the error message
- Calls
os.Exit(1)
to terminate the program
Advantages
- Clean Logging: Allows you to provide a meaningful error message before exit
- Controlled Shutdown: Predictably exits with status code 1
- Application-Friendly: Ideal for application-level fatal errors
- Testing Friendly: Being a standard library function makes it easier to test
- User-Friendly: Error messages are typically more readable for end users
Disadvantages
- No Deferred Functions: Bypasses deferred function calls
- Limited Debug Info: Doesn’t automatically provide stack traces
- No Recovery: Once called, the program will exit without possibility of recovery
- Scope Limited: Only affects the current program
Understanding panic
panic
is a built-in Go function designed to handle exceptional conditions that shouldn’t occur during normal operation. When called, it:
- Immediately stops normal execution
- Runs all deferred functions in the current goroutine
- Prints a stack trace
- Terminates the program (unless recovered)
Advantages
- Deferred Function Handling: Executes all deferred functions before terminating
- Rich Debug Information: Provides detailed stack traces by default
- Recoverable: Can be caught and handled using
recover()
- Programming Error Detection: Excellent for catching logical errors and invariant violations
- Library-Friendly: Allows calling code to handle the panic if desired
Disadvantages
- Abrupt Termination: Less graceful than logging and exiting
- Potentially Confusing: Can be misused for normal error conditions
- Verbose Stack Traces: Can overwhelm users with technical details
- Unpredictable Propagation: May bubble up through multiple function calls
When to Use Each
Use log.Fatalf() when:
// Application startup failures
if err := initDatabase(); err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
// Configuration errors
if config.APIKey == "" {
log.Fatalf("API key not provided in configuration")
}
// Critical resource unavailability
file, err := os.Open("critical-data.json")
if err != nil {
log.Fatalf("Cannot proceed without critical data: %v", err)
}
Use panic when:
// Programming errors
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
// Invariant violations
func processUser(u *User) {
if u == nil {
panic("user cannot be nil")
}
// Process user...
}
// Unrecoverable state corruption
func (s *Stack) Pop() interface{} {
if s.IsEmpty() {
panic("pop from empty stack")
}
// Pop item...
}
Best Practices
Library Code
- Prefer returning errors over either
log.Fatalf()
orpanic
- Use
panic
only for programming errors - Never use
log.Fatalf()
in libraries
- Prefer returning errors over either
Application Code
- Use
log.Fatalf()
for application-level fatal errors - Reserve
panic
for truly exceptional conditions - Consider using custom logging solutions for better control
- Use
Error Handling Strategy
- Document your error handling strategy
- Be consistent across your codebase
- Consider your users when choosing between the two
Conclusion
The choice between log.Fatalf()
and panic
depends largely on your specific use case. For application-level fatal errors where you want to log clearly and exit cleanly, log.Fatalf()
is the better choice. For programming errors or invariant violations that should never happen in correct code, panic
is more appropriate.
Remember that in many cases, especially in library code, returning an error is preferable to either option, as it gives callers more control over error handling and program flow.