Error Handling in Go
Learn how to handle errors in Go using the `error` interface and the `defer`, `panic`, and `recover` mechanisms.
Error Handling in Go
Introduction
Error handling is a crucial aspect of writing robust and reliable Go programs. Go provides a simple yet powerful mechanism for managing errors using the error
interface and the defer
, panic
, and recover
statements. This document outlines the fundamental concepts of error handling in Go.
The error
Interface
In Go, errors are represented by the built-in error
interface. This interface is defined as:
type error interface {
Error() string
}
Any type that implements the Error() string
method satisfies the error
interface. Functions that can potentially fail typically return an error
value as the last return value. If the function succeeds, the error value is typically nil
; otherwise, it contains a description of the error.
Example
package main
import (
"fmt"
"errors"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
Handling Errors with if err != nil
The most common way to handle errors in Go is to check if the error value returned by a function is not nil
. If it's not nil
, it indicates an error occurred, and you should handle it appropriately, such as logging the error, returning an error up the call stack, or taking corrective action.
Custom Error Types
You can create custom error types by defining a struct or other type that implements the error
interface. This allows you to attach more information to the error, such as the specific file or line number where the error occurred.
Example
package main
import (
"fmt"
)
type MyError struct {
Message string
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("Error Code: %d, Message: %s", e.Code, e.Message)
}
func doSomething(input int) error {
if input < 0 {
return &MyError{Message: "Input cannot be negative", Code: 1001}
}
return nil
}
func main() {
err := doSomething(-5)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Success!")
}
}
defer
, panic
, and recover
Go provides a mechanism for handling exceptional situations using panic
and recover
. defer
is also heavily used in conjunction with these.
defer
: Adefer
statement schedules a function call to be run after the surrounding function returns. This is commonly used to ensure resources are released, such as closing files or database connections, regardless of whether the function returns normally or due to a panic.panic
: Apanic
occurs when the program encounters a critical error that it cannot recover from. When apanic
occurs, the program stops executing the current function and begins unwinding the stack, executing any deferred functions along the way.recover
: Arecover
function can be used to regain control after apanic
. It can only be called directly within a deferred function. Ifrecover
is called within a deferred function that's executing because of apanic
,recover
stops the panicking sequence by restoring normal execution and returns the error value passed topanic
. Ifrecover
is called when there is no panic, it returnsnil
.
Example
package main
import (
"fmt"
)
func mightPanic() {
panic("A problem occurred!")
}
func doSomethingSafe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
mightPanic()
fmt.Println("This will not be printed if mightPanic panics.") // Unreachable if panic occurs.
}
func main() {
doSomethingSafe()
fmt.Println("Program continues after recovering from panic.")
}
Important Considerations:
- Avoid using
panic
andrecover
for normal error handling. They are generally reserved for truly exceptional situations where the program cannot continue safely. recover
can only be used effectively within a deferred function.- Overuse of
panic
/recover
can make code harder to understand and debug.
Error Wrapping (Go 1.13 and later)
Go 1.13 introduced standard support for error wrapping, allowing you to provide more context to errors without losing the original error's information. This is done using the fmt.Errorf
function with the %w
verb.
Example
package main
import (
"fmt"
"errors"
)
func readData() error {
// Simulate an underlying error
err := errors.New("failed to read from disk")
return fmt.Errorf("reading data failed: %w", err) // Wrap the original error
}
func processData() error {
err := readData()
if err != nil {
return fmt.Errorf("processing data failed: %w", err) // Wrap the error further
}
return nil
}
func main() {
err := processData()
if err != nil {
fmt.Println("An error occurred:", err)
// Check if the error chain contains a specific error
if errors.Is(err, errors.New("failed to read from disk")) {
fmt.Println("Root cause: Failed to read from disk")
}
}
}
Key benefits of error wrapping:
- Contextual Information: Provides a stack of errors, making it easier to trace the origin of the problem.
- Error Inspection: Allows you to inspect the error chain to determine if a specific error is present using
errors.Is
anderrors.As
.