Secure error handling involves several key principles: minimizing information leakage, logging errors appropriately, and ensuring that error messages do not reveal sensitive application logic. This tutorial will cover these principles with practical examples.

1. Avoiding Information Leakage

One of the primary goals of secure error handling is to avoid leaking sensitive information through error messages. This includes stack traces, internal logic, and database queries. Instead, provide generic error messages to users while logging detailed errors for developers.

Example: Generic User Error Messages

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    _, err := someOperation()
    if err != nil {
        // Log the detailed error for developers
        log.Printf("Error occurred: %v", err)
        // Return a generic error message to the user
        http.Error(w, "An unexpected error occurred. Please try again later.", http.StatusInternalServerError)
        return
    }
    fmt.Fprintln(w, "Operation successful!")
}

func someOperation() (string, error) {
    return "", fmt.Errorf("this is a detailed error message")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

In this example, the detailed error is logged for developers, while the user receives a generic message that does not disclose sensitive information.

2. Centralized Error Handling

Centralizing error handling can help maintain consistency across your application. By using middleware or a custom error handler, you can ensure that all errors are processed in a uniform manner.

Example: Custom Error Handler Middleware

package main

import (
    "fmt"
    "log"
    "net/http"
)

type errorHandler struct {
    next http.Handler
}

func (h *errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("Recovered from error: %v", err)
            http.Error(w, "An unexpected error occurred. Please try again later.", http.StatusInternalServerError)
        }
    }()
    h.next.ServeHTTP(w, r)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        panic("this is a panic error")
    })

    log.Fatal(http.ListenAndServe(":8080", &errorHandler{next: mux}))
}

In this example, the custom error handler middleware captures panics and logs them, while providing a secure response to the user.

3. Logging Best Practices

When logging errors, it’s important to include relevant context without exposing sensitive information. Use structured logging to capture key details.

Example: Structured Logging

package main

import (
    "log"
    "os"
)

func main() {
    logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)

    err := someOperation()
    if err != nil {
        logger.Printf("Error: %v | UserID: %d | Operation: %s", err, 12345, "someOperation")
    }
}

func someOperation() error {
    return fmt.Errorf("this is a detailed error message")
}

In this structured logging example, the error message is logged along with relevant context, such as the user ID and operation type, without revealing sensitive application details.

4. User-Friendly Error Handling

While it’s essential to avoid leaking sensitive information, it’s also important to provide users with meaningful feedback. This can be achieved by categorizing errors and providing specific messages based on the type of error.

Example: Categorized Error Messages

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    err := someOperation()
    if err != nil {
        switch err {
        case ErrNotFound:
            http.Error(w, "Resource not found", http.StatusNotFound)
        case ErrUnauthorized:
            http.Error(w, "Unauthorized access", http.StatusUnauthorized)
        default:
            http.Error(w, "An unexpected error occurred. Please try again later.", http.StatusInternalServerError)
        }
        return
    }
    fmt.Fprintln(w, "Operation successful!")
}

var (
    ErrNotFound      = fmt.Errorf("not found")
    ErrUnauthorized   = fmt.Errorf("unauthorized")
)

func someOperation() error {
    return ErrNotFound // Simulating an error
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

In this example, different error types are handled with specific messages, improving user experience while maintaining security.

Summary

Implementing secure error handling in Go is essential for protecting sensitive information while ensuring a good user experience. By avoiding information leakage, centralizing error handling, following logging best practices, and providing user-friendly messages, you can create a robust error handling strategy.

PrincipleDescription
Avoid Information LeakageUse generic messages for users; log detailed errors for developers.
Centralized Error HandlingImplement middleware to handle errors consistently across the application.
Logging Best PracticesUse structured logging to capture context without exposing sensitive data.
User-Friendly Error HandlingCategorize errors and provide meaningful feedback without revealing details.

Learn more with useful resources: