Why You Should Avoid Directly Returning Err in Go
In Go, error handling is a core design philosophy. Through explicit error return values (the error
type), developers must face potential problems head-on. However, many developers new to Go (and even experienced developers) often make a mistake: directly returning the original err
. This seemingly simple behavior actually buries hidden dangers for code debugging and maintenance.
Problems with Directly Returning err
1. Opaque Error Information
When you directly return err
in multi-layer nested function calls, upper-level callers may have no idea where the error originated:
func ReadConfig() error {
data, err := os.ReadFile("config.yaml")
if err != nil {
return err // Directly return the original error
}
// Parse configuration...
}
If the file doesn’t exist, the error message might be:
open config.yaml: no such file or directory
But the code calling ReadConfig
might have no idea which step went wrong. The error information lacks context!
2. Difficult Debugging
Suppose your service returns an io.EOF
error when accessing the database, but there might be dozens of places in the system that could trigger io.EOF
. At this point, with only the original error type, you cannot quickly locate the root cause of the problem.
3. Broken Error Chain
Go 1.13 introduced the Error Wrapping mechanism, allowing bottom-level errors to be wrapped into high-level errors through the %w
verb. If you directly return err
, you’re essentially giving up this tracing capability.
Solutions: Using fmt.Errorf
or errors.New
Method 1: Add Context Through fmt.Errorf
func ReadConfig() error {
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Parse configuration...
}
Now the error message becomes:
failed to read config file: open config.yaml: no such file or directory
Through the %w
verb, the original error is completely preserved and can be traced layer by layer through errors.Unwrap()
.
Method 2: Use errors.New
for Static Errors
For simple errors that don’t need dynamic information, directly use errors.New
:
var ErrInvalidInput = errors.New("invalid input format")
func Validate(input string) error {
if len(input) == 0 {
return ErrInvalidInput
}
// Other validation logic...
}
Best Practices for Error Handling
-
Always Add Context
Every error return point should clearly state the current operation target. -
Use
%w
to Wrap Bottom-Level Errors
Maintain the complete error chain, making it convenient for callers to useerrors.Is
anderrors.As
for judgment. -
Avoid Redundant Information
Error messages should be concise and specific, for example:// Not recommended fmt.Errorf("error: failed to open file: %v", err) // Recommended fmt.Errorf("open file: %w", err)
-
Unify Error Format
Team-wide agreement on error message style (such as starting with verbs: “open file” vs. “failed to open file”).
Example Comparison
Suppose an HTTP handler function calls ReadConfig
:
func handler(w http.ResponseWriter, r *http.Request) {
if err := ReadConfig(); err != nil {
log.Printf("Error: %v", err)
w.WriteHeader(500)
return
}
// ...
}
-
Directly returning
err
Log output:Error: open config.yaml: no such file or directory
Developers need to check layer by layer where file operations were called. -
Using
fmt.Errorf
Log output:Error: failed to read config file: open config.yaml: no such file or directory
Directly pinpoints the problem at the configuration reading stage.
Conclusion
In Go, an excellent error message should be like a clue in a detective novel—it should not only point out the problem but also provide enough context to help developers quickly solve the case. By wrapping errors with fmt.Errorf
and errors.New
, you can:
✅ Significantly improve log readability
✅ Accelerate the debugging process
✅ Maintain the integrity of the error chain
Remember: code is not only written for machines to execute, but also for future yourself and other developers to read. Good error handling habits are an important investment in code maintainability.
// Start taking action now!
if err != nil {
return fmt.Errorf("your turn: %w", err)
}