Middle · Gin services

Recovery middleware versus business rollback

Recovery catches a panic and returns 500 after a database operation. Which guarantees can you honestly report?

Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.

EXAMPLE · FRAGMENT UNLESS A COMPLETE PROGRAM IS SHOWN

go
package main

import (
	"errors"
	"fmt"
	"net/http/httptest"

	"github.com/gin-gonic/gin"
)

var errMissing = errors.New("missing")

func main() {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.Use(func(c *gin.Context) {
		c.Next()
		if c.Writer.Written() || len(c.Errors) == 0 {
			return
		}
		status := 500
		if errors.Is(c.Errors.Last().Err, errMissing) {
			status = 404
		}
		c.JSON(status, gin.H{"error": "request failed"})
	})
	r.GET("/orders/:id", func(c *gin.Context) { _ = c.Error(errMissing); c.Abort() })
	w := httptest.NewRecorder()
	r.ServeHTTP(w, httptest.NewRequest("GET", "/orders/7", nil))
	fmt.Println(w.Code, w.Body.String())
}
Reveal a strong answer
  1. Framework recovery can keep a panic from crashing a request-serving goroutine, but it cannot undo a committed database write or decide whether a remote operation succeeded. Expected application errors should travel through a deliberate mapping policy. Assign one layer to produce the response, and test both its status and public body rather than checking only that the handler returned.

  2. Use table-driven requests with method, path, input, verified identity and expected response. Check status, headers, JSON shape and whether forbidden side effects occurred. Inject a narrow application service and test the real storage adapter separately. Add panic recovery as a last-resort boundary, plus structured logging, without using panic as routine validation control flow. The route records an application error and stops the pending handler chain. After Next returns, middleware maps a known missing value to 404 and writes once if no earlier layer committed the response. The public body is deliberately stable and does not expose a database error. Logging the detailed cause belongs at a protected reporting boundary.

  3. A test that asserts only status 200 can miss a leaked secret body or a second write after an error. Returning an error from a helper does not automatically attach it to Gin. Recovery is not a transaction manager. Test malformed and unauthorized requests, missing resources, dependency cancellation, unknown outcomes and a panic after a simulated effect.

Common trap

Recovery is not a transaction coordinator and an HTTP error is not evidence of no side effects.

Try it

Write an error-mapping test that checks status, public body and whether the application side effect was already committed.

Worked solution

You can report that the panic was handled at a request boundary, not that storage or external effects were rolled back. Expected errors should have a deliberate mapping and one response writer owner. Test the response body for accidental information exposure and test transaction/effect recovery separately. Neither a nil handler return nor a 500 tells the full business outcome after a partial operation.

Change the constraint

What should the client do after a write timeout whose outcome cannot be determined from the response?

How did you do?

Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.

Not marked. Progress stays on this browser, separate from course completion.

Primary references

Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.