Episode ini merapikan sisi respons gagal: HTTPError dan custom error handler, format RFC 9457 Problem Details untuk API error yang konsisten, logging terstruktur dengan slog melalui RequestLogger, serta integrasi log dengan aggregator seperti Loki dan ELK.

API yang sehat tidak hanya menangani keberhasilan — dia merancang kegagalan. Konsumen API perlu tahu apa yang salah, mengapa, dan bagaimana memperbaikinya, dengan format yang konsisten. Di sisi lain, developer butuh log yang bisa dicari dan dianalisis mesin.
Episode 11 ini merapikan kedua sisi tersebut: HTTPError dan custom error handler, format RFC 9457 Problem Details untuk API error, logging terstruktur dengan slog via RequestLogger, dan integrasi dengan log aggregator.
Echo mewakili error sebagai echo.HTTPError — sebuah error yang membawa status code, pesan, dan payload tambahan:
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "user tidak ditemukan")
}Saat handler mengembalikan echo.HTTPError, Echo otomatis menulis status code dan pesan ke response. Handler tidak perlu tahu detail HTTP — cukup kembalikan error.
HTTPError punya field Details untuk data kontekstual, dan Message bisa diisi struktur kompleks:
err := echo.NewHTTPError(http.StatusBadRequest)
err.Message = "validasi gagal"
err.Details = []string{"email harus format valid", "nama minimal 3 karakter"}
return errField Details diserialisasi ke response, memberi klien informasi yang bisa dipakai untuk memperbaiki request.
Semua error — dari handler, binding, validator, maupun router — akhirnya melewati satu titik: e.HTTPErrorHandler. Ganti dengan implementasi sendiri untuk kontrol penuh:
e.HTTPErrorHandler = func(err error, c echo.Context) {
he, ok := err.(*echo.HTTPError)
if !ok {
he = echo.NewHTTPError(http.StatusInternalServerError, "internal server error")
}
if err := c.JSON(he.Code, he.Message); err != nil {
e.Logger.Error("gagal menulis respons error", "err", err)
}
}Error non-HTTP dibungkus menjadi 500 agar informasi internal tidak bocor ke klien. Logging detail error asli dilakukan terpisah, tidak dikirim ke response.
RFC 9457 mendefinisikan format konsisten untuk error HTTP: satu objek JSON dengan member standar seperti type, title, status, detail, dan instance:
{
"type": "https://example.com/problems/validation",
"title": "Validasi gagal",
"status": 400,
"detail": "Satu atau lebih field tidak valid",
"instance": "/api/v1/users",
"errors": ["email harus format valid"]
}Implementasikan dalam custom error handler dengan memetakan HTTPError ke struktur Problem Details:
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
Instance string `json:"instance"`
}
e.HTTPErrorHandler = func(err error, c echo.Context) {
he, ok := err.(*echo.HTTPError)
if !ok {
he = echo.NewHTTPError(http.StatusInternalServerError)
}
problem := Problem{
Type: "https://example.com/problems/" + http.StatusText(he.Code),
Title: http.StatusText(he.Code),
Status: he.Code,
Detail: fmt.Sprint(he.Message),
Instance: c.Path(),
}
if err := c.JSON(he.Code, problem); err != nil {
e.Logger.Error("gagal menulis problem", "err", err)
}
}Dengan format ini, seluruh error API kalian memiliki skema yang sama — mudah dipahami klien dan mudah dipetakan ke dokumentasi.
Di episode 3 kalian sudah memakai RequestLogger. Di production, atur level dan handler output slog sesuai environment:
level := slog.LevelInfo
if cfg.Environment == "development" {
level = slog.LevelDebug
}
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
})))NewJSONHandler menghasilkan log JSON yang mudah di-parse mesin. Di development, LevelDebug memberi visibilitas lebih.
Manfaatkan slog dalam handler untuk mencatat konteks spesifik domain:
slog.Info("user dibuat",
"user_id", user.ID,
"by", c.Get("user_id"),
"trace_id", c.Response().Header().Get(echo.HeaderXRequestID),
)Biasakan menyertakan request_id di setiap log agar satu request bisa dilacak dari ujung ke ujung — fondasi observability di episode 18.
Log JSON di stdout siap diarahkan ke aggregator seperti Loki atau ELK tanpa kode tambahan:
services:
api:
image: belajar-echo:latest
logging:
driver: json-file
promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log
command: -config.file=/etc/promtail/config.yamlKarena log sudah terstruktur, aggregator bisa langsung membuat label dari field seperti status dan latency tanpa parsing manual.
Episode 11 merapikan respons gagal dan jejak digital aplikasi: HTTPError menjadi bahasa error terpusat, custom error handler mengontrol respons final, RFC 9457 Problem Details memberi skema error konsisten, slog menghasilkan log terstruktur, dan aggregator menerima log tersebut tanpa perubahan kode.
Inti yang harus dibawa pulang:
HTTPError membawa status code dan pesan.e.HTTPErrorHandler adalah satu titik kontrol seluruh error.slog dengan JSON handler menghasilkan log siap mesin.request_id di setiap log untuk pelacakan.Di episode 12 selanjutnya kita akan membahas context, timeout & concurrency — c.Request().Context() untuk nilai dan pembatalan, goroutine di dalam handler, context timeout, sinkronisasi state dengan mutex, dan pola aman saat berbagi data antar request.