On this page

Error Handling

MuxMaster provides a structured approach to error handling that eliminates boilerplate in individual handlers while giving you full control over how errors are serialized and logged.

Table of Contents


The Problem with Standard Handlers

A http.HandlerFunc has no return value, so error handling is manual:

mux.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(muxmaster.PathParam(r, "id"))
    if err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }
    user, err := db.FindUser(id)
    if err != nil {
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    if err := json.NewEncoder(w).Encode(user); err != nil {
        log.Printf("encode error: %v", err)
    }
})

Every handler repeats the same pattern: check error, write response, return. The error format (plain text in this case) must be kept consistent manually across all handlers.


HandlerFuncE

HandlerFuncE extends the standard handler signature with an error return:

type HandlerFuncE func(http.ResponseWriter, *http.Request) error

Handlers return nil on success, or an error to be handled centrally:

mux.GETE("/users/:id", func(w http.ResponseWriter, r *http.Request) error {
    id, err := muxmaster.ParamsFromContext(r.Context()).Int("id")
    if err != nil {
        return muxmaster.Error(http.StatusBadRequest, err)
    }
    user, err := db.FindUser(id)
    if err != nil {
        return muxmaster.Error(http.StatusNotFound, errors.New("user not found"))
    }
    return muxmaster.JSON(w, http.StatusOK, user)
})

The same pattern applies to every HTTP method: GETE, POSTE, PUTE, PATCHE, DELETEE, HEADE, OPTIONSE.


HTTPError

muxmaster.Error(code, err) wraps an error with an HTTP status code:

// Create an HTTPError
err := muxmaster.Error(http.StatusNotFound, errors.New("user not found"))

// Check status code
var he muxmaster.HTTPError
if errors.As(err, &he) {
    fmt.Println(he.StatusCode()) // 404
}

HTTPError is an interface:

type HTTPError interface {
    error
    StatusCode() int
}

Any error that implements this interface is recognized by MuxMaster's error-handling pipeline, including custom implementations:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string   { return e.Field + ": " + e.Message }
func (e *ValidationError) StatusCode() int { return http.StatusUnprocessableEntity }

The Default Error Handler

When no ErrorHandler is set, MuxMaster's default behaviour is:

  • If the error implements HTTPError, respond with that status code and the error message as plain text.
  • Otherwise, respond with 500 Internal Server Error.

Custom Error Handler

Set mux.ErrorHandler to take over all error responses globally:

mux.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
    code := http.StatusInternalServerError
    msg  := "internal server error"

    var he muxmaster.HTTPError
    if errors.As(err, &he) {
        code = he.StatusCode()
        msg  = err.Error()
    } else {
        // Log unexpected errors; do not leak internal details to the client
        log.Printf("unhandled error [%s %s]: %v", r.Method, r.URL.Path, err)
    }

    muxmaster.JSON(w, code, map[string]string{"error": msg})
}

The ErrorHandler is called for every HandlerFuncE that returns a non-nil error, across the entire mux and all its groups.

Structured error responses

For APIs that need machine-readable errors:

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Detail  string `json:"detail,omitempty"`
}

mux.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
    apiErr := APIError{
        Code:    http.StatusInternalServerError,
        Message: "internal server error",
    }

    var he muxmaster.HTTPError
    if errors.As(err, &he) {
        apiErr.Code    = he.StatusCode()
        apiErr.Message = err.Error()
    }

    var ve *ValidationError
    if errors.As(err, &ve) {
        apiErr.Detail = "field: " + ve.Field
    }

    muxmaster.JSON(w, apiErr.Code, apiErr)
}

Error-Returning Method Variants

Every standard HTTP method has an error-returning variant:

Standard Error-returning
mux.GET mux.GETE
mux.POST mux.POSTE
mux.PUT mux.PUTE
mux.PATCH mux.PATCHE
mux.DELETE mux.DELETEE
mux.HEAD mux.HEADE
mux.OPTIONS mux.OPTIONSE

The same variants exist on *Group:

api := mux.Group("/api/v1")
api.POSTE("/users", createUser)
api.GETE("/users/:id", getUser)
api.DELETEE("/users/:id", deleteUser)

Custom 404 and 405 Handlers

Not Found (404)

Called when no route matches the request path:

mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    muxmaster.JSON(w, http.StatusNotFound, map[string]string{
        "error": "the requested resource does not exist",
        "path":  r.URL.Path,
    })
})

Method Not Allowed (405)

Called when the path is registered for at least one method, but not the requested method. MuxMaster sets the Allow header automatically:

mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    allowed := w.Header().Get("Allow")
    muxmaster.JSON(w, http.StatusMethodNotAllowed, map[string]string{
        "error":   "method not allowed",
        "allowed": allowed,
    })
})

To enable 405 responses, HandleMethodNotAllowed must be true (the default).


Panic Recovery

MuxMaster does not automatically recover from panics. Use middleware.Recoverer to catch panics before they crash the server:

mux.Use(middleware.Recoverer)

For custom panic handling, set mux.PanicHandler:

mux.PanicHandler = func(w http.ResponseWriter, r *http.Request, rcv any) {
    log.Printf("panic recovered [%s %s]: %v\n%s",
        r.Method, r.URL.Path, rcv, debug.Stack())
    muxmaster.JSON(w, http.StatusInternalServerError, map[string]string{
        "error": "an unexpected error occurred",
    })
}

PanicHandler receives:

  • w http.ResponseWriter — the response writer
  • r *http.Request — the request that caused the panic
  • rcv any — the value passed to panic()

Patterns and Best Practices

Wrap sentinel errors early

Define your domain errors using muxmaster.Error at the service boundary, so handlers never need to know status codes:

// In your repository or service layer
var ErrUserNotFound = muxmaster.Error(http.StatusNotFound, errors.New("user not found"))
var ErrUserExists   = muxmaster.Error(http.StatusConflict,  errors.New("user already exists"))

// In the handler — no status code knowledge needed
mux.POSTE("/users", func(w http.ResponseWriter, r *http.Request) error {
    user, err := userService.Create(payload)
    if err != nil {
        return err // ErrUserExists passes through to ErrorHandler with 409
    }
    return muxmaster.JSON(w, http.StatusCreated, user)
})

Distinguish client errors from server errors in the error handler

mux.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
    var he muxmaster.HTTPError
    if errors.As(err, &he) {
        if he.StatusCode() >= 500 {
            log.Printf("server error: %v", err) // log server errors
        }
        muxmaster.JSON(w, he.StatusCode(), map[string]string{"error": err.Error()})
        return
    }
    log.Printf("unexpected error: %v", err) // always log unexpected errors
    muxmaster.JSON(w, http.StatusInternalServerError, map[string]string{
        "error": "internal server error",
    })
}

Use errors.As to unwrap chains

muxmaster.Error wraps the original error, so you can unwrap the chain:

base := errors.New("record not found")
he   := muxmaster.Error(http.StatusNotFound, base)

errors.Is(he, base) // true — unwraps through the HTTPError wrapper

See Also

Upstream source

HandlerFuncE, the default ErrorHandler, and the JSON / Text / XML helpers used for error rendering live in handler.go and response.go in the upstream repository.

Common questions

How do I return an error from a handler instead of writing the response myself?

Use mux.HandlerFuncE instead of http.HandlerFunc. The signature is func(http.ResponseWriter, *http.Request) error; returning a non-nil error invokes the configured ErrorHandler, which is responsible for translating the error to an HTTP response.

How do I customise the error-to-response translation?

Set mux.Config.ErrorHandler (or pass mux.WithErrorHandler at construction) to a function that inspects the error and writes the response. The default handler returns 500 with a plain-text body; production services typically branch on errors.As/errors.Is to map domain errors to specific status codes and content types.

How does MuxMaster handle panics in handlers?

MuxMaster does not recover panics by default — that responsibility belongs to a middleware so the policy is explicit. Add the built-in Recoverer (or a custom equivalent) via m.Use(...) early in the chain; the middleware logs the panic and writes a 500.