go / vizerror
I use a "visible error" type to separate user-facing messages from internal error details.
The problem
Internal errors often contain information that's confusing or inappropriate for end users:
// Too technical for users
"pq: duplicate key value violates unique constraint \"users_email_key\""
// What users should see
"An account with that email already exists"
But you still want the full error for logging and debugging.
The pattern
package vizerror
import "errors"
// Error is safe to display to end users.
type Error struct {
publicErr error // shown to users
wrapped error // for logging/debugging
}
func (e Error) Error() string {
return e.publicErr.Error()
}
func (e Error) Unwrap() error {
return e.wrapped
}
// New creates a user-visible error.
func New(publicMsg string) error {
err := errors.New(publicMsg)
return Error{publicErr: err, wrapped: err}
}
// WrapWithMessage wraps an internal error with a user-visible message.
func WrapWithMessage(wrapped error, publicMsg string) error {
return Error{
publicErr: errors.New(publicMsg),
wrapped: wrapped,
}
}
// As extracts a vizerror from an error chain.
func As(err error) (Error, bool) {
var e Error
ok := errors.As(err, &e)
return e, ok
}
Usage
In application code, wrap internal errors:
func CreateUser(email string) error {
err := db.Insert(user)
if isUniqueViolation(err) {
return vizerror.WrapWithMessage(err,
"An account with that email already exists")
}
if err != nil {
return vizerror.WrapWithMessage(err,
"Unable to create account. Please try again.")
}
return nil
}
In your HTTP handler or CLI, check for visible errors:
func handleError(w http.ResponseWriter, err error) {
// Log the full error for debugging
slog.Error("request failed", "error", err)
// Show user-safe message if available
if ve, ok := vizerror.As(err); ok {
http.Error(w, ve.Error(), http.StatusBadRequest)
return
}
// Generic message for unexpected errors
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
Benefits
- Security: Internal details (table names, SQL, stack traces) stay in logs
- UX: Users see helpful, actionable messages
- Debugging:
Unwrap()preserves the full error chain forerrors.Is/As
When to use
- HTTP APIs where error messages are shown to users
- CLI tools with user-facing output
- Any boundary between internal code and external consumers
See caller for adjusting slog source locations when logging these errors from wrapper functions.