As unfortunate as it is, when developing API servers in Go, there are inevitably situations where Panic occurs.
Common examples include:
- Using a method on a nil pointer receiver
- Accessing a non-existent index of an array
- Failing type assertions
- Concurrent writing to a map
- Writing to a closed channel
In such cases, if you’re using the standard library net/http
, the server handles the Panic. For instance, if you write the code as follows, the server will by default recover from the Panic and return an empty response:
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "Some logic 1 executed")
slog.InfoContext(r.Context(), "Some logic 2 executed")
slog.InfoContext(r.Context(), "Panic occurred")
panic("Something went wrong")
w.Write([]byte("Hello, World!"))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", SomeHandler())
port := ":8080"
slog.Info("Starting server", "port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
2025/03/19 12:10:49 INFO Some logic 1 executed
2025/03/19 12:10:49 INFO Some logic 2 executed
2025/03/19 12:10:49 INFO Panic occurred
2025/03/19 12:10:49 http: panic serving [::1]:58424: Something went wrong
goroutine 78 [running]:
net/http.(*conn).serve.func1()
/opt/homebrew/opt/go/libexec/src/net/http/server.go:1947 +0xb0
panic({0x102508980?, 0x10256dd90?})
/opt/homebrew/opt/go/libexec/src/runtime/panic.go:792 +0x124
main.main.SomeHandler.func1({0x14000108ae8?, 0x1022756a0?}, 0x1400017af00)
/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:44 +0x9c
net/http.HandlerFunc.ServeHTTP(0x1400013a0c0?, {0x1025720b8?, 0x1400016cc40?}, 0x102417484?)
/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38
net/http.(*ServeMux).ServeHTTP(0x0?, {0x1025720b8, 0x1400016cc40}, 0x1400017af00)
/opt/homebrew/opt/go/libexec/src/net/http/server.go:2822 +0x1b4
net/http.serverHandler.ServeHTTP({0x140001f4420?}, {0x1025720b8?, 0x1400016cc40?}, 0x1?)
/opt/homebrew/opt/go/libexec/src/net/http/server.go:3301 +0xbc
net/http.(*conn).serve(0x14000147290, {0x1025724c8, 0x14000117080})
/opt/homebrew/opt/go/libexec/src/net/http/server.go:2102 +0x52c
created by net/http.(*Server).Serve in goroutine 1
/opt/homebrew/opt/go/libexec/src/net/http/server.go:3454 +0x3d8
What about other languages and frameworks?
Other languages behave similarly. For instance, when a null pointer exception occurs without proper error handling, the program inevitably terminates. However, many modern frameworks like Spring handle such errors at the framework level to prevent server termination.
Is it okay to leave it as is?
While the default behavior is decent, simply sending a blank response to a frontend developer or user can be quite inconsiderate. When an error occurs, it’s common practice to explicitly specify what error occurred and send an appropriate error response.
For a 500 INTERNAL_SERVER_ERROR
, you might send a response like this:
{
"error": "Internal Server Error",
"message": "Something went wrong"
}
While it might be tempting to blame the net/http
standard library for not returning such responses, given the variety of server environments, it likely chooses the most generic and conservative approach.
To tackle this issue in Go, middleware can be utilized.
What is Middleware?
As briefly mentioned in a previous post on logging request IDs in a Go API server, middleware refers to code that operates between requests and responses.
Typically, middleware is used to implement common functionalities such as logging, authentication, and error handling in an API. By composing middleware in a chain form, APIs can remain focused on business logic.
Here’s a diagram:
flowchart LR subgraph Middleware1 subgraph Middleware2 subgraph Middleware...N API end end end client1[Client] -->|Request| Middleware1 Middleware1 -->|Response| client2[Client]
Implementation
A typical middleware in Go can be implemented as follows:
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do something before
next.ServeHTTP(w, r)
// Do something after
})
}
Here, the next
parameter points to the Handler
containing the next middleware or actual API logic.
To invoke it, you can do:
func main() {
http.Handle("/", Middleware(http.HandlerFunc(Handler)))
http.ListenAndServe(":8080", nil)
}
To chain several middleware, you might use:
http.Handle("/", Middleware1(Middleware2(http.HandlerFunc(Handler))))
Panic Recover Middleware
Now, let’s implement middleware that recovers from Panic.
Depending on your requirements, you can implement it in a simple way. I chose to use the slog package to log the incident and return a 500 Internal Server Error:
func PanicMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
// Do not recover http.ErrAbortHandler.
// In this case, no response is sent to the client, and it is not logged.
panic(r)
}
slog.Error("HTTP handler panicked", "err", r, "stacktrace", string(debug.Stack()))
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
slog.Info("Panic Recovered")
return
}
}()
next.ServeHTTP(w, r)
})
}
When a Panic occurs and is captured by r := recover()
, its content is logged using the slog package, and a 500 Internal Server Error is returned.
Particularly, http.ErrAbortHandler
is not recovered. This is a special error used by http.Error
, and if it’s thrown, no response is sent to the client nor is it logged.
Its description is as follows:
// ErrAbortHandler is a sentinel panic value to abort a handler.
// While any panic from ServeHTTP aborts the response to the client,
// panicking with ErrAbortHandler also suppresses logging of a stack
// trace to the server's error log.
var ErrAbortHandler = errors.New("net/http: abort Handler")
Result
Let’s test the middleware:
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "Some logic 1 executed")
slog.InfoContext(r.Context(), "Some logic 2 executed")
slog.InfoContext(r.Context(), "Panic occurred")
panic("Something went wrong")
w.Write([]byte("Hello, World!"))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", PanicMiddleware(SomeHandler()))
port := ":8080"
slog.Info("Starting server", "port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
Write the handler and main code as above and connect to http://localhost:8080 to see the following output:
2025/03/19 11:33:31 INFO Starting server port=:8080
2025/03/19 11:33:39 INFO Some logic 1 executed
2025/03/19 11:33:39 INFO Some logic 2 executed
2025/03/19 11:33:39 INFO Panic occurred
2025/03/19 11:33:39 ERROR HTTP handler panicked err="Something went wrong" stacktrace="goroutine 35 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:26 +0x64\nmain.main.PanicMiddleware.func2.1()\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:19 +0x70\npanic({0x1004809c0?, 0x1004e5e70?})\n\t/opt/homebrew/opt/go/libexec/src/runtime/panic.go:792 +0x124\nmain.main.SomeHandler.func1({0x1400000e0e0?, 0x1400000e0d4?}, 0x1400007e000)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:44 +0x9c\nnet/http.HandlerFunc.ServeHTTP(0x100b1cce8?, {0x1004ea198?, 0x14000198000?}, 0x10020184c?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nmain.main.PanicMiddleware.func2({0x1004ea198?, 0x14000198000?}, 0x14000163b28?)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:32 +0x6c\nnet/http.HandlerFunc.ServeHTTP(0x1400013a0c0?, {0x1004ea198?, 0x14000198000?}, 0x10038f5a4?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nnet/http.(*ServeMux).ServeHTTP(0x14000182000?, {0x1004ea198, 0x14000198000}, 0x1400007e000)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2822 +0x1b4\nnet/http.serverHandler.ServeHTTP({0x14000072030?}, {0x1004ea198?, 0x14000198000?}, 0x6?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3301 +0xbc\nnet/http.(*conn).serve(0x140001462d0, {0x1004ea5a8, 0x14000115080})\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2102 +0x52c\ncreated by net/http.(*Server).Serve in goroutine 1\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3454 +0x3d8"
2025/03/19 11:33:39 INFO Panic Recovered
2025/03/19 11:33:39 INFO Some logic 1 executed
2025/03/19 11:33:39 INFO Some logic 2 executed
2025/03/19 11:33:39 INFO Panic occurred
2025/03/19 11:33:39 ERROR HTTP handler panicked err="Something went wrong" stacktrace="goroutine 35 [running]:\nruntime/debug.Stack()\n\t/opt/homebrew/opt/go/libexec/src/runtime/debug/stack.go:26 +0x64\nmain.main.PanicMiddleware.func2.1()\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:19 +0x70\npanic({0x1004809c0?, 0x1004e5e70?})\n\t/opt/homebrew/opt/go/libexec/src/runtime/panic.go:792 +0x124\nmain.main.SomeHandler.func1({0x1400000e1b0?, 0x140000140e4?}, 0x1400007e140)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:44 +0x9c\nnet/http.HandlerFunc.ServeHTTP(0x100b1cce8?, {0x1004ea198?, 0x140001980e0?}, 0x10020184c?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nmain.main.PanicMiddleware.func2({0x1004ea198?, 0x140001980e0?}, 0x14000163b28?)\n\t/Users/giraffewithcode/dev/personal/awesomeProject7/main.go:32 +0x6c\nnet/http.HandlerFunc.ServeHTTP(0x1400013a0c0?, {0x1004ea198?, 0x140001980e0?}, 0x10038f5a4?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2294 +0x38\nnet/http.(*ServeMux).ServeHTTP(0x140001820a0?, {0x1004ea198, 0x140001980e0}, 0x1400007e140)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2822 +0x1b4\nnet/http.serverHandler.ServeHTTP({0x14000072030?}, {0x1004ea198?, 0x140001980e0?}, 0x6?)\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3301 +0xbc\nnet/http.(*conn).serve(0x140001462d0, {0x1004ea5a8, 0x14000115080})\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:2102 +0x52c\ncreated by net/http.(*Server).Serve in goroutine 1\n\t/opt/homebrew/opt/go/libexec/src/net/http/server.go:3454 +0x3d8"
2025/03/19 11:33:39 INFO Panic Recovered
If you set the panic value to http.ErrAbortHandler
, it behaves as follows:
panic(http.ErrAbortHandler)
2025/03/19 11:43:17 INFO Some logic 1 executed
2025/03/19 11:43:17 INFO Some logic 2 executed
2025/03/19 11:43:17 INFO Panic occurred
The Panic is not handled, so no stack trace is logged and no logs are recorded. However, you notice that net/http
still performs a basic recovery to prevent the server from shutting down.