Episode ini menyatukan penanganan error dan logging Gin: mencatat error dengan c.Error, membuat custom error type, middleware error handler yang mengembalikan response JSON terpadu, serta integrasi slog untuk structured logging di middleware Gin.

Aplikasi yang berjalan mulus tanpa error hanyalah angan-angan. Yang membedakan aplikasi matang adalah cara ia menangani error dan mencatat kejadian. Episode 11 ini membedah error handling terpusat di Gin: menampung error dengan c.Error, mendefinisikan custom error type yang membawa status HTTP, middleware yang mengubahnya menjadi response JSON terpadu, dan structured logging dengan slog dari pustaka standar Go.
Dengan pendekatan ini, handler tidak perlu mengulang pola if err != nil yang menulis JSON serampangan. Satu middleware menjadi penjaga terakhir: menerjemahkan semua error menjadi format konsisten dan mencatatnya ke log untuk debugging.
Handler sering harus meneruskan error ke lapisan atas. Gin menyediakan c.Error(err) untuk menyimpan error ke dalam context, sementara penulisan response bisa diserahkan ke middleware:
func getUserHandler(c *gin.Context) {
user, err := h.svc.GetUser(c.Request.Context(), id)
if err != nil {
c.Error(err)
return
}
c.JSON(200, user)
}c.Error(err) menambahkan error ke slice c.Errors dan menandai response belum ditulis. Handler berhenti, dan middleware yang terdaftar nanti membaca c.Errors untuk menentukan response. Perhatikan bahwa c.Error tidak secara otomatis mengembalikan status code — itu tugas middleware.
Agar middleware tahu status code dan pesan yang aman dikirim ke klien, definisikan tipe error sendiri:
type AppError struct {
Code string
Message string
HTTPCode int
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Err.Error()
}
return e.Message
}
func NotFound(msg string) *AppError {
return &AppError{Code: "not_found", Message: msg, HTTPCode: 404}
}AppError mengimplementasikan interface error lewat method Error(). Field HTTPCode membawa status HTTP, Code membawa kode mesin yang stabil untuk klien, dan Err membungkus error asli untuk logging. Constructor seperti NotFound(msg) mempermudah pembuatan error yang konsisten.
Error bawaan Go tetap bisa dibungkus dengan fmt.Errorf dan ditelusuri dengan errors.As:
if err := repo.FindByID(ctx, id); err != nil {
return nil, fmt.Errorf("repo: %w", err)
}var appErr *AppError
if errors.As(err, &appErr) {
c.JSON(appErr.HTTPCode, gin.H{
"code": appErr.Code,
"message": appErr.Message,
})
return
}
c.JSON(500, gin.H{"code": "internal", "message": "terjadi kesalahan"})errors.As(err, &appErr) memeriksa apakah error (atau bungkusannya) bertipe *AppError. Jika ya, response memakai kode dan status dari AppError; jika bukan, fallback ke 500 dengan pesan generik agar detail internal tidak bocor ke klien.
Gabungkan semua potongan menjadi satu middleware utuh:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) == 0 {
return
}
err := c.Errors.Last().Err
if c.Writer.Written() {
return
}
var appErr *AppError
if errors.As(err, &appErr) {
c.JSON(appErr.HTTPCode, gin.H{
"code": appErr.Code,
"message": appErr.Message,
})
return
}
slog.Error("unhandled error", "path", c.Request.URL.Path, "error", err)
c.JSON(500, gin.H{"code": "internal", "message": "terjadi kesalahan"})
}
}Penjaga c.Writer.Written() mencegah middleware menimpa response yang sudah ditulis handler. Error yang tidak dikenal dicatat dengan slog.Error sebelum response 500 generik dikirim — pola ini menjaga log tetap informatif dan response tetap aman.
Go 1.21+ menyediakan log/slog untuk structured logging. Buat logger JSON di main:
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("server started", "port", cfg.Port)
slog.Error("database unreachable", "addr", cfg.DatabaseURL)slog.NewJSONHandler(os.Stdout, nil) menghasilkan log dalam format JSON — satu baris per kejadian, mudah dibaca mesin seperti Loki, CloudWatch, atau ELK. Level bisa dinaikkan ke slog.LevelDebug saat development melalui LOG_LEVEL dari episode 10.
Integrasikan slog ke alur request:
func slogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
slog.Info("request",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"status", c.Writer.Status(),
"duration_ms", time.Since(start).Milliseconds(),
)
}
}c.Writer.Status() diambil setelah handler selesai sehingga status final tercatat. Field duration_ms berguna untuk memantau endpoint lambat. Kombinasikan dengan middleware error handler di atas: satu log untuk request, satu log untuk error yang tidak tertangani.
Inti yang harus dibawa pulang:
c.Error(err) menampung error di context, bukan menulis response langsung.c.Errors.Last().Err membaca error terakhir di middleware.AppError membawa kode, pesan, dan status HTTP.errors.As membedakan error yang dikenal dari error tak dikenal.slog memberikan structured logging JSON yang siap dipakai production.Di episode 12 selanjutnya kita akan membedah context, timeout & concurrency — memakai c.Request.Context, c.Copy untuk goroutine, context timeout, rate limiting sederhana, serta sinkronisasi akses data dengan mutex.