This episode covers error handling in Fiber v3: the default ErrorHandler, custom error handlers, the fiber.NewError function, the difference between fiber.Error and fiber.RecoverError, and the Logger middleware with a custom template and time format.

Error handling is what separates an annoying API from a professional one. Episode 9 covers error handling in Fiber v3: how the default ErrorHandler works, how to write a custom error handler, and how fiber.NewError helps you return structured errors.
We'll also master the Logger middleware — customizing output format, choosing what information gets recorded, and setting the time format. The combination of both makes your application easy to debug and easy to monitor.
When a handler returns an error, Fiber forwards it to the ErrorHandler. By default, Fiber responds with the status from the error, or status 500 for a plain error:
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return fiber.NewError(fiber.StatusNotFound, "Halaman tidak ada")
})In Fiber v3, the default ErrorHandler is passed a *fiber.Error. A 404 status is responded with the body {"code":404,"message":"Halaman tidak ada"} — complete with code and message. If a handler returns a plain error, the default status is 500.
Fiber v3 defines two internal error types. Recognizing them matters when writing an error handler:
type Error struct {
Code int
Message string
}
type RecoverError struct {
Code int
Message string
}fiber.Error holds Code and Message for errors returned by handlers. fiber.RecoverError is a special wrapper for errors caught by the Recover middleware from panics. Both can be distinguished with errors.As inside a custom error handler.
You can replace the global error handler via configuration. This is important for ensuring a consistent response format across the whole application:
app := fiber.New(fiber.Config{
ErrorHandler: func(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
var fe *fiber.Error
if errors.As(err, &fe) {
code = fe.Code
}
if code == fiber.StatusNotFound {
return c.Status(code).SendString("404 - halaman tidak ditemukan")
}
return c.Status(code).JSON(fiber.Map{
"code": code,
"message": err.Error(),
})
},
})The custom handler receives c fiber.Ctx and err error. It can inspect the error type, determine the status code, and choose the response format — for example an HTML page for 404 and JSON for API errors.
Error handlers don't have to be global. Fiber v3 lets you set a route-specific error handler with fiber.RouteAttrErrorHandler:
app.Get("/admin/:id", func(c fiber.Ctx) error {
return fiber.NewError(fiber.StatusForbidden, "akses ditolak")
}, fiber.RouteAttrErrorHandler(func(c fiber.Ctx, err error) error {
return c.Status(fiber.StatusUnauthorized).SendString("login dulu")
}))With RouteAttrErrorHandler, errors from the /admin/:id route are handled by the dedicated handler — the global handler still applies to other routes. This is useful for areas with different response contracts, for example an API and web pages in one application.
The fiber.NewError(code, message) function creates a *fiber.Error with a status code and message. This error can carry extra arguments:
func findUser(id int) (*User, error) {
if id < 1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "ID tidak valid", "id", id)
}
user, ok := users[id]
if !ok {
return nil, fiber.NewError(fiber.StatusNotFound, "User tidak ditemukan")
}
return &user, nil
}fiber.NewError(400, "...", "id", id) inserts argument pairs for additional context. When this error is forwarded to the ErrorHandler, the argument values can be accessed to build a richer response message.
logger.New() has many template options. You can write your own format using variables like {{pid}}, {{ip}}, {{method}}, {{path}}, {{status}}, and {{latency}}:
app.Use(logger.New(logger.Config{
Format: "[{{time}}] {{ip}} {{method}} {{path}} status={{status}} lat={{latency}}\n",
TimeFormat: "2006-01-02 15:04:05",
TimeZone: "Asia/Jakarta",
}))The template determines what gets recorded. TimeFormat sets the time format, TimeZone sets the timezone — UTC by default. This combination produces logs that are easy to read and consistent with the local zone.
curl http://localhost:3000/tidak-ada
curl http://localhost:3000/admin/1The first request triggers the global custom error handler — the 404 - halaman tidak ditemukan page. The second request triggers the admin-specific RouteAttrErrorHandler with the login dulu response. In the terminal, the logger middleware records both requests with the custom template.
Key takeaways:
ErrorHandler returns a *fiber.Error with code and message.fiber.Config{ErrorHandler}.fiber.NewError(code, message, args...) creates structured errors.fiber.Error and fiber.RecoverError can be distinguished with errors.As.fiber.RouteAttrErrorHandler provides a per-route error handler.logger middleware can be customized via Format, TimeFormat, and TimeZone.In the next episode, episode 10, we discuss context and request extensions — all the methods on fiber.Ctx, proxy forwarding and sendStream, and request extensions like special values on port and namespace.