Skip to content

V5 unable capture error #2869

Description

@zuozhehao
	e := echo.New()
	e.Use(middleware.Recover())
	e.Use(middleware.RequestLogger())

	e.GET("/error", func(c *echo.Context) error {
		return c.Render(http.StatusOK, "error", nil)
	})

	e.HTTPErrorHandler = func(c *echo.Context, err error) {
		// Handler error

		resp, err := echo.UnwrapResponse(c.Response())
		if resp != nil && resp.Committed {
			return
		}

		slog.Info("err", "err", err) // err is nil
		_ = c.JSON(http.StatusOK, nil)
	}

	_ = e.Start(":1323")
}

RequestLogger: "error":"renderer not registered"
HTTPErrorHandler : err = nil

{"time":"2026-01-20T12:03:34.7921301+08:00","level":"ERROR","msg":"REQUEST_ERROR","method":"GET","uri":"/error","status":200,"latency":0,"host":"localhost:1323","bytes_in":"","bytes_out":5,"user_agent":"curl/8.16.0","remote_ip":"::1","request_id":"","error":"renderer not registered"}
2026/01/20 12:03:34 INFO err err=<nil>

Activity

  1. changed the title [-]V5 Response committed no effect[/-] [+]V5 HTTPErrorHandler unable capture error[/+] on Jan 20, 2026
  2. changed the title [-]V5 HTTPErrorHandler unable capture error[/-] [+]V5 unable capture error[/+] on Jan 20, 2026
  3. aldas commented on Jan 20, 2026

    @aldas
    Contributor

    You probably solved it yourself but resp, err := echo.UnwrapResponse(c.Response()) shadows the function argument err variable. Better way to do it would be to this

    		if resp, _ := echo.UnwrapResponse(c.Response()); resp != nil && resp.Committed {
    			return
    		}

    Default error handler is implemented like that:

    echo/echo.go

    Lines 365 to 413 in 9500f27

    func DefaultHTTPErrorHandler(exposeError bool) HTTPErrorHandler {
    return func(c *Context, err error) {
    if r, _ := UnwrapResponse(c.response); r != nil && r.Committed {
    return
    }
    code := http.StatusInternalServerError
    var sc HTTPStatusCoder
    if errors.As(err, &sc) {
    if tmp := sc.StatusCode(); tmp != 0 {
    code = tmp
    }
    }
    var result any
    switch m := sc.(type) {
    case json.Marshaler: // this type knows how to format itself to JSON
    result = m
    case *HTTPError:
    sText := m.Message
    if sText == "" {
    sText = http.StatusText(code)
    }
    msg := map[string]any{"message": sText}
    if exposeError {
    if wrappedErr := m.Unwrap(); wrappedErr != nil {
    msg["error"] = wrappedErr.Error()
    }
    }
    result = msg
    default:
    msg := map[string]any{"message": http.StatusText(code)}
    if exposeError {
    msg["error"] = err.Error()
    }
    result = msg
    }
    var cErr error
    if c.Request().Method == http.MethodHead { // Issue #608
    cErr = c.NoContent(code)
    } else {
    cErr = c.JSON(code, result)
    }
    if cErr != nil {
    c.Logger().Error("echo default error handler failed to send error to client", "error", cErr) // truly rare case. ala client already disconnected
    }
    }
    }

    and _ = c.JSON(http.StatusOK, nil) would output {} to the client. so this probably should be

    _ = c.JSON(http.StatusOK, map[string]string{"error": err.Error()})
  4. zuozhehao commented on Jan 20, 2026

    @zuozhehao
    Author

    Yes, this is a small issue that is easy to overlook.
    In version V4, resp, err := echo.UnwrapResponse(c.Response()) is not needed, and err will not be overwritten.
    I am upgrading from version V4 to version V5.

  5. aldas commented on Jan 20, 2026

    @aldas
    Contributor

    I have note about it to the release notes.

    @zuozhehao , what was the most complex part and what most timeconsuming when upgrading from v4 to v5?

  6. zuozhehao commented on Jan 20, 2026

    @zuozhehao
    Author

    @aldas According to the upgrade documentation, the upgrade was completed quickly, and all features worked normally during gray (canary) testing.

  7. zuozhehao commented on Jan 20, 2026

    @zuozhehao
    Author

    In V4, Recover provides a dedicated handler with the type
    type LogErrorFunc func(c echo.Context, err error, stack []byte) error,
    allowing panic handling and stack retrieval to be done independently.

    In V5, this no longer exists; everything is handled through HTTPErrorHandler func(c echo.Context, err error).

    Since the panic is wrapped using
    fmt.Errorf("[PANIC RECOVER] %w %s", tmpErr, stack[:length]),
    it is currently not convenient to determine whether an error is caused by a panic, nor to reliably extract the stack trace.

    @aldas Is there any good recommendation or solution for this.

  8. aldas commented on Jan 20, 2026

    @aldas
    Contributor

    Are you just logging that stack? I think we can bring that handler function back.

  9. zuozhehao commented on Jan 20, 2026

    @zuozhehao
    Author

    @aldas Yes, at the moment it is not possible to accurately identify the error type within HTTPErrorHandler.

    If we change tmpErr to a custom error type, such as PanicError, would that be an appropriate approach.

    stack := make([]byte, config.StackSize)
    length := runtime.Stack(stack, !config.DisableStackAll)
    tmpErr = fmt.Errorf("[PANIC RECOVER] %w %s", tmpErr, stack[:length])

  10. aldas commented on Jan 20, 2026

    @aldas
    Contributor

    @zuozhehao I have merged #2871. I'll tag new patch release in couple of days but you can use it if you add direct commit as dependency. go get -u github.com/labstack/echo/v5@096ce41d2a24f5ec652b21adbcf25c484bef49ea

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions