Basics of Error Handling
Go’s approach to error handling is different from many other programming languages. Instead of using exceptions, Go uses explicit error returns. This means that functions return errors as part of their result, and it’s the caller’s responsibility to handle them. This approach leads to clear, maintainable, and predictable code.
The error
Type
In Go, the error
type is a built-in interface. An error is anything that implements this interface:
type error interface {
Error() string
}
This means any type that has an Error()
method with the appropriate signature satisfies the error
interface.
Creating and Returning Errors
You can create errors using the errors
package. Here’s a simple way to create an error:
import "errors"
var err = errors.New("this is an error")
In functions, you often return an error as the second return value:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
In the divide
function, if b
is zero, it returns an error indicating “division by zero”. Otherwise, it returns the result of the division and nil
for no error.
Handling Errors
To handle an error, you check if the returned error is not nil
:
result, err := divide(4, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
This pattern is common in Go code. You frequently see functions returning a value and an error, and the caller immediately checks if the error is nil
.
Custom Error Types
Sometimes, you might want to provide more context with your errors. You can do this by defining a custom error type:
type DivideError struct {
A, B float64
Msg string
}
func (e *DivideError) Error() string {
return fmt.Sprintf("cannot divide %f by %f: %s", e.A, e.B, e.Msg)
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, &DivideError{A: a, B: b, Msg: "division by zero"}
}
return a / b, nil
}
func main() {
result, err := divide(4, 0)
if err != nil {
fmt.Println("Error:", err)
if divErr, ok = err.(*DivideError); ok {
fmt.Printf("Details: A=%f, B=%f\n", divErr.A, divErr.B)
}
} else {
fmt.Println("Result:", result)
}
}
In this example:
- We define a custom error type
DivideError
with fields for the operands and a message. - The
Error
method formats a descriptive error message. - The
divide
function returns aDivideError
if there’s an attempt to divide by zero. - In
main
, we check the error and type-assert it to get more details.
Wrapping Errors
The fmt.Errorf
function can be used to wrap errors with additional context:
err := fmt.Errorf("failed to open file: %w", originalErr)
The %w
verb is used to wrap the original error with the new message.
Example Putting It All Together
Here’s a more comprehensive example incorporating everything we’ve discussed:
package main
import (
"errors"
"fmt"
)
// Custom error type
type DivideError struct {
A, B float64
Msg string
}
func (e *DivideError) Error() string {
return fmt.Sprintf("cannot divide %f by %f: %s", e.A, e.B, e.Msg)
}
// Function to divide two numbers
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, &DivideError{A: a, B: b, Msg: "division by zero"}
}
return a / b, nil
}
func main() {
// Attempt to divide two numbers
result, err := divide(4, 0)
if err != nil {
fmt.Println("Error:", err)
if divErr, ok = err.(*DivideError); ok {
fmt.Printf("Details: A=%f, B=%f\n", divErr.A, divErr.B)
}
} else {
fmt.Println("Result:", result)
}
// Wrap the error with additional context
_, originalErr := divide(4, 0)
wrappedErr := fmt.Errorf("additional context: %w", originalErr)
fmt.Println("Wrapped error:", wrappedErr)
}
This example demonstrates how to define custom errors, handle them, and wrap them with additional context.
Go’s error handling approach encourages explicit error checking, making your code predictable and easier to maintain. It might seem verbose at first, but it leads to robust and clear code.