fiber 不提供 express 风格的四参数错误中间件,因其采用 go 原生错误处理模型:依赖 defer-recover 捕获 panic,handler 返回的 error 仅用于流程控制,不自动触发 http 错误响应;必须手动注册中间件 recover panic 并统一格式化响应,同时通过 c.locals 透传业务错误,在末尾中间件统一处理,确保 panic 和业务错误共用同一响应结构。

Fiber 没有内置的全局错误处理器,必须手动注册 app.Use 中间件捕获 panic,并用 c.Next() 后检查 c.Response().StatusCode() 或自定义错误类型来统一响应格式。
为什么 Fiber 不像 Express 那样有 app.use((err, req, res, next) => {})
Fiber 的错误处理模型是 Go 原生风格:不依赖多参数错误中间件签名,而是靠 recover + 显式错误传递。它默认不会把 handler 返回的 error 自动转成 500 响应——你得自己判断、拦截、格式化。常见误区是以为写 return errors.New("boom") 就能被自动捕获,其实只会让 Fiber 记录日志并返回空响应(或 200),状态码不变。
- handler 函数签名是
func(*fiber.Ctx) error,但这个error只用于控制流程(比如提前退出),Fiber 不会主动渲染它 - 真正触发 HTTP 错误响应的,是你调用的
c.Status(500).JSON(...)这类方法 - panic 才是唯一会被中间件
recover()拦截的“未预期崩溃”,比如 nil pointer dereference、map write to nil
用中间件捕获 panic 并统一返回 JSON 错误
这是最常被遗漏的一环:没加 recover,协程 panic 就直接炸掉整个请求 goroutine,还可能泄露堆栈。必须在最外层中间件里做:
app.Use(func(c *fiber.Ctx) error {
defer func() {
if r := recover(); r != nil {
// 记录结构化日志(含堆栈)
log.Printf("panic recovered: %v, stack: %s", r, debug.Stack())
// 统一返回 500
_ = c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "internal server error",
"code": "INTERNAL_ERROR",
})
}
}()
return c.Next()
})
- 一定要放在
app.Use()最前面,否则后续中间件 panic 就捕获不到 - 别在 defer 里调
c.SendString()—— 此时响应头可能已写,会 panic -
c.Next()必须显式调用,否则后续路由不执行;它的返回值是 handler 的error,但你通常不需要处理它(除非你要区分业务 error 和 panic)
如何让业务错误也走同一套响应逻辑
不要依赖 handler 返回的 error,改用自定义错误类型 + ctx.Locals 透传,再在末尾中间件统一处理:
type AppError struct {
Code string
Message string
Status int
}
func NewAppError(code, msg string, status int) *AppError {
return &AppError{Code: code, Message: msg, Status: status}
}
// 在 handler 里:
if user == nil {
c.Locals("error", NewAppError("USER_NOT_FOUND", "user not exist", fiber.StatusNotFound))
return c.Next() // 不 return error,让流程走到统一出口
}
- 所有业务错误都塞进
c.Locals("error"),然后调c.Next()继续往下走 - 在最后一个中间件(注册顺序最后)里检查:
if err, ok := c.Locals("error").(*AppError); ok { c.Status(err.Status).JSON(err) } - 这样就能和 panic 路径共用同一套 JSON schema,前端不用区分两种错误来源
- 注意:别在中间件里重复设置状态码,
c.Status()是幂等的,但多次调用可能覆盖前值
别忽略中间件顺序和 c.IsAborted()
如果某个中间件(如鉴权)已经写了 401 响应,后续中间件仍可能执行并再次写响应,导致 http: multiple response.WriteHeader calls panic。正确做法是:
- 每个中间件开头加
if c.IsAborted() { return nil },快速退出 - 或者更稳妥:在统一错误中间件里,只对尚未写响应的请求生效 ——
if c.Response().StatusCode() == 0才处理 - 中间件注册顺序决定执行链,
app.Use(authMw)必须在app.Use(errorHandler)之前,否则 auth 失败的 401 就绕过错误处理了
真正容易被忽略的是:错误中间件无法挽回已发生的副作用。比如 DB insert 成功后 panic,recover 能拦住响应,但事务不会回滚 —— 这部分必须靠业务层自己检查 c.IsAborted() 做补偿清理。











