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

  1. Always Add Context
    Every error return point should clearly state the current operation target.

  2. Use %w to Wrap Bottom-Level Errors
    Maintain the complete error chain, making it convenient for callers to use errors.Is and errors.As for judgment.

  3. 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)
  4. 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)
}
0%