Episode ini membangun sistem error handling yang terpusat: custom error type dengan status code, handler yang mengembalikan error ke JSON, dan middleware recovery untuk menangkap panic. Kalian juga mengadopsi slog untuk logging terstruktur dan membuat request logging middleware.

Handler yang meng-copy-paste blok http.Error dengan pesan berbeda-beda menghasilkan API yang tidak konsisten dan log yang tidak berguna. Episode 11 membangun sistem yang terpusat: semua error melewati jalur yang sama, status code konsisten, dan setiap request tercatat dalam log terstruktur.
Go menawarkan log/slog sejak versi 1.21 — logger terstruktur bawaan yang tidak butuh dependency tambahan. Kombinasi error type custom, adapter handler, dan slog akan membuat debugging production terasa jauh lebih ringan.
Buat tipe error yang membawa informasi HTTP:
type APIError struct {
Code int
Message string
Err error
}
func (e *APIError) Error() string {
return e.Message
}
func NewAPIError(code int, msg string) *APIError {
return &APIError{Code: code, Message: msg}
}NewAPIError(code, msg) menciptakan error yang tahu status code-nya. Handler cukup mengembalikan error; middleware atau adapter yang menerjemahkannya ke respons.
func NewAPIErrorf(code int, msg string, err error) *APIError {
return &APIError{Code: code, Message: msg, Err: err}
}errors.Is dan errors.As tetap berfungsi saat error dibungkus dengan %w karena APIError mengimplementasikan Unwrap. Ini menjaga rantai error tetap bisa dilacak.
Daripada mengubah kontrak http.Handler, bungkus handler yang mengembalikan error:
func Handle(fn func(w http.ResponseWriter, req *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if err := fn(w, req); err != nil {
var apiErr *APIError
if errors.As(err, &apiErr) {
writeJSON(w, apiErr.Code,
map[string]string{"error": apiErr.Message})
return
}
slog.Error("unexpected error", "error", err)
writeJSON(w, http.StatusInternalServerError,
map[string]string{"error": "internal server error"})
}
}
}errors.As(err, &apiErr) memeriksa apakah error adalah APIError. Bila bukan, log sebagai error tak terduga dan kembalikan 500 — kesalahan detail tidak pernah bocor ke klien.
Dengan adapter, handler tetap bersih:
r.Get("/users/{id}", Handle(func(w http.ResponseWriter, req *http.Request) error {
id, _ := strconv.Atoi(chi.URLParam(req, "id"))
user, err := repo.FindByID(req.Context(), id)
if err != nil {
return NewAPIError(http.StatusNotFound, "user tidak ditemukan")
}
writeJSON(w, http.StatusOK, user)
return nil
}))Handle(...) membungkus handler sehingga logika error terpusat di satu tempat, bukan tersebar di setiap handler.
middleware.Recoverer mencegah server mati karena panic:
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)
r.Use(requestLogger)r.Use(middleware.Recoverer) menangkap panic di handler, mencetak stack trace ke log, dan mengembalikan status 500 ke klien. Server tetap hidup melayani request berikutnya.
Untuk mengembalikan JSON pada panic:
func jsonRecoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
if r := recover(); r != nil {
slog.Error("panic tertangkap",
"recovered", r, "path", req.URL.Path)
writeJSON(w, http.StatusInternalServerError,
map[string]string{"error": "internal server error"})
}
}()
next.ServeHTTP(w, req)
})
}recover() menangkap nilai panic sebelum menyebar, lalu middleware menulis respons JSON yang konsisten.
log/slog menghasilkan JSON yang siap diparsing:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)slog.NewJSONHandler(os.Stdout, nil) mengeluarkan log berformat JSON — bisa dibaca manusia maupun mesin. slog.SetDefault membuat seluruh package memakai logger yang sama.
Buat middleware yang mencatat setiap request:
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
next.ServeHTTP(w, req)
slog.Info("request selesai",
"method", req.Method,
"path", req.URL.Path,
"durasi", time.Since(start))
})
}slog.Info("request selesai", "method", req.Method, "path", req.URL.Path) mencatat atribut terstruktur — bukan string yang di-sprintf. Ini yang membuat log bisa difilter dan di-agregasi.
slog.Debug untuk detail yang hanya dibutuhkan saat debugging.slog.Info untuk alur normal seperti request selesai.slog.Warn untuk kondisi mencurigakan yang belum fatal.slog.Error untuk kegagalan yang perlu diperhatikan manusia.Inti yang harus dibawa pulang:
APIError membawa status code; errors.As mendeteksinya.Handle memusatkan terjemahan error ke JSON.middleware.Recoverer mencegah server mati karena panic.slog bawaan Go menulis log JSON terstruktur.Di episode 12 selanjutnya kita mengeksplorasi request lifecycle: context, timeout, dan concurrency — nilai request-scoped, pembatalan request, middleware.Timeout, goroutine di handler, serta middleware.Throttle dan sinkronisasi state bersama.