fiber.New() 初始化时必须通过 fiber.Config{ErrorHandler:} 显式注册错误处理器,该处理器仅对未被内层 recover 捕获的 panic 生效,且不可动态覆盖;自定义 recover 中间件需将 defer 放在 c.Next() 前,goroutine 内 panic 需单独防护,ErrorHandler 与中间件 recover 各司其职。

fiber.New() 初始化时必须传入 ErrorHandler
Fiber 不会自动捕获 handler 中的 panic,必须在创建应用实例时通过 fiber.Config{ErrorHandler: } 显式注册兜底逻辑。这个函数只对当前请求 goroutine 有效,且仅在 panic 未被更内层 recover 捕获时触发。
常见错误是后期调用 app.Use() 或单独给某个路由加中间件来“补救”,但此时 ErrorHandler 已失效——它只在 fiber.New() 阶段生效,不能动态覆盖。
推荐写法:
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) {
log.Printf("500 PANIC in %s %s: %v", c.Method(), c.Path(), err)
c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
},
})
中间件里 defer+recover 必须放在 next.ServeHTTP() 前
如果你自己写 recover 中间件(比如为了记录 traceID 或统一返回格式),defer 语句必须出现在 next.ServeHTTP() 调用之前,否则 panic 发生时该 defer 还没注册,根本不会执行。
容易踩的坑包括:
- 把
defer func() { recover() }()写在next.ServeHTTP()后面 - 提前把
recover()结果赋值给变量(如r := recover()),再判断——这会导致永远为nil - 在
w.Header().WasWritten()为 true 后仍调用c.Status().SendString(),引发二次 panic
正确结构:
func recovery() fiber.Handler {
return func(c *fiber.Ctx) {
defer func() {
if r := recover(); r != nil {
buf := debug.Stack()
log.Printf("PANIC: %v\n%s", r, buf)
if !c.Response().Header.Has("Content-Type") {
c.Status(fiber.StatusInternalServerError).SendString("500")
}
}
}()
c.Next() // ← defer 必须在这之前注册
}
}
goroutine 内部的 panic 无法被外层 recover 捕获
这是最常被忽略的一点:你在 handler 里写了 defer recover,但里面起了一个 go func() { panic("x") }(),这个 panic 永远不会被 handler 的 defer 捕获。
原因很简单:recover 只作用于**当前 goroutine**。子 goroutine 崩溃后,主 goroutine 继续跑,日志里可能只有一行 panic: x,没有上下文、没有 traceID、请求却已静默失败。
解决方案只有两个:
- 所有异步逻辑都用封装好的
safeGo()启动,内部自带defer recover - 业务代码中避免在 handler 里裸写
go func(),改用带 recover 的协程池或任务队列
示例 safeGo:
func safeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v", r)
}
}()
f()
}()
}
ErrorHandler 和中间件 recover 的职责边界要分清
ErrorHandler 是 Fiber 最终兜底,它只处理两类情况:一是 handler 函数本身 panic 且未被任何 recover 拦截;二是 panic 发生在 Fiber 内部(比如路由匹配阶段)。它不处理中间件里已 recover 过的错误,也不会重复触发。
而自定义中间件里的 defer+recover 更适合做精细控制:比如记录完整堆栈、关联请求 ID、触发告警、或返回结构化 JSON 错误体。
两者可以共存,但要注意顺序和责任划分:
- 如果中间件已经
c.Status(500).JSON(...),就别让ErrorHandler再写响应,否则会 panic - 若中间件只是记录日志并
return,没写响应头,那ErrorHandler仍会接管 - 不要在
ErrorHandler里再起 goroutine 并期望它能 recover —— 那个新 goroutine 依然需要自己的defer recover
真正难的不是写 recover,而是确保每个可能 panic 的执行路径——无论是主流程、中间件、还是任意一个 go 语句——都有对应的防护。漏掉任何一个,就是静默崩溃点。


















