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

When to use

See caller for adjusting slog source locations when logging these errors from wrapper functions.

← All articles