Pola penanganan error API Go untuk respons yang jelas dan konsisten
Pola penanganan error API Go untuk menstandarkan error bertipe, status HTTP, request ID, dan pesan aman tanpa membocorkan detail internal.

Mengapa error API yang tidak konsisten membuat klien frustrasi
Saat setiap endpoint melaporkan kegagalan dengan cara berbeda, klien berhenti mempercayai API Anda. Satu route mengembalikan { "error": "not found" }, yang lain mengembalikan { "message": "missing" }, dan yang ketiga mengirim teks biasa. Meski maksudnya mirip, kode klien jadi harus menebak apa yang terjadi.
Biayanya terlihat cepat. Tim membangun logika parsing yang rapuh dan menambah kasus khusus per endpoint. Retry menjadi berisiko karena klien tidak bisa membedakan “coba lagi nanti” dari “input Anda salah.” Tiket dukungan meningkat karena klien hanya melihat pesan samar, dan tim Anda tidak mudah mencocokkannya dengan baris log server.
Skenario umum: aplikasi mobile memanggil tiga endpoint saat signup. Yang pertama mengembalikan HTTP 400 dengan peta error per field, yang kedua mengembalikan HTTP 500 dengan string stack trace, dan yang ketiga mengembalikan HTTP 200 dengan { "ok": false }. Tim app mengirim tiga handler error berbeda, dan tim backend masih mendapat laporan seperti “signup kadang gagal” tanpa petunjuk jelas mulai dari mana.
Tujuannya adalah satu kontrak yang dapat diprediksi. Klien harus bisa dengan andal membaca apa yang terjadi — apakah itu kesalahan mereka atau server, apakah retry masuk akal, dan ada request ID yang bisa mereka kirim ke dukungan.
Catatan ruang lingkup: ini fokus pada API HTTP berformat JSON (bukan gRPC), tetapi ide yang sama berlaku di mana pun Anda mengembalikan error ke sistem lain.
Target sederhana: satu kontrak yang diikuti setiap endpoint
Pilih satu kontrak yang jelas untuk error dan pastikan setiap endpoint mematuhinya. “Konsisten” berarti bentuk JSON sama, arti field sama, dan perilaku sama tidak peduli handler mana yang gagal. Setelah itu terjadi, klien berhenti menebak dan mulai menangani error.
Kontrak yang berguna membantu klien memutuskan apa yang harus dilakukan selanjutnya. Untuk sebagian besar aplikasi, setiap respons error harus menjawab tiga pertanyaan:
- Bisakah saya memperbaiki input saya?
- Haruskah saya mencoba lagi nanti?
- Perlukah saya menghubungi dukungan?
Sekumpulan aturan praktis:
- Satu skema respons untuk semua error.
- Satu kebijakan status-code (tipe error yang sama selalu memetakan ke HTTP status yang sama).
- Satu kebijakan pesan aman (apa yang boleh dilihat pengguna vs yang tetap internal).
- Satu hook korelasi (request ID dikembalikan sehingga dukungan bisa menemukan kegagalan).
Tentukan sejak awal apa yang tidak boleh muncul di respons. Item “jangan pernah” umum meliputi fragmen SQL, stack trace, hostname internal, secret, dan string error mentah dari dependency.
Pertahankan pemisahan yang bersih: pesan singkat untuk pengguna (aman, sopan, dapat ditindaklanjuti) dan detail internal (error penuh, stack, konteks) disimpan di log. Contoh: “Could not save your changes. Please try again.” aman. “pq: duplicate key value violates unique constraint users_email_key” tidak aman.
Ketika setiap endpoint mengikuti kontrak yang sama, klien bisa membuat satu handler error dan menggunakannya ulang di mana-mana.
Definisikan skema respons error yang dapat diandalkan klien
Klien hanya bisa menangani error dengan baik jika setiap endpoint menjawab dalam bentuk yang sama. Pilih satu envelope JSON dan pertahankan kestabilannya.
Default praktis adalah objek error ditambah request_id di tingkat atas:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
},
"request_id": "req_01HV..."
}
Status HTTP memberikan kategori umum (400, 401, 409, 500). error.code yang bisa dibaca mesin memberikan kasus spesifik yang bisa di-branch oleh klien. Pemisahan itu penting karena banyak masalah berbeda berbagi status yang sama. Aplikasi mobile mungkin menampilkan UI berbeda untuk EMAIL_TAKEN vs WEAK_PASSWORD, meski keduanya 400.
Jaga error.message agar aman dan manusiawi. Itu harus membantu pengguna memperbaiki masalah, tapi jangan bocorkan internal (SQL, stack trace, nama provider, path file).
Field opsional berguna selama tetap dapat diprediksi:
- Error validasi:
details.fieldssebagai peta field ke pesan. - Batas laju atau masalah sementara:
details.retry_after_seconds. - Panduan tambahan:
details.docs_hintsebagai teks biasa (bukan URL).
Untuk kompatibilitas mundur, anggap nilai error.code sebagai bagian kontrak API Anda. Tambah kode baru tanpa mengubah arti lama. Hanya tambahkan field opsional, dan anggap klien akan mengabaikan field yang tidak dikenal.
Error bertipe di Go: model bersih untuk handler Anda
Penanganan error menjadi berantakan ketika setiap handler menemukan cara sendiri untuk memberi sinyal kegagalan. Sekelompok kecil error bertipe memperbaiki itu: handler mengembalikan tipe error yang diketahui, dan satu lapisan respons mengubahnya menjadi respons yang konsisten.
Set starter praktis mencakup sebagian besar endpoint:
- ValidationError (input buruk)
- NotFoundError (resource tidak ada)
- ConflictError (kontraint unik, mismatch state)
- UnauthorizedError (tidak login atau tidak diizinkan)
- InternalError (semua lainnya)
Kuncinya adalah kestabilan di tingkat atas, meski penyebab akar berubah. Anda bisa membungkus error level rendah (SQL, jaringan, parsing JSON) sambil tetap mengembalikan tipe publik yang sama yang dapat dideteksi middleware.
type NotFoundError struct {
Resource string
ID string
Err error // private cause
}
func (e NotFoundError) Error() string { return "not found" }
func (e NotFoundError) Unwrap() error { return e.Err }
Di handler Anda, kembalikan NotFoundError{Resource: "user", ID: id, Err: err} alih-alih membocorkan sql.ErrNoRows secara langsung.
Untuk memeriksa error, utamakan errors.As untuk tipe kustom dan errors.Is untuk sentinel error. Sentinel error (seperti var ErrUnauthorized = errors.New("unauthorized")) bekerja untuk kasus sederhana, tapi tipe kustom unggul ketika Anda butuh konteks aman (mis. resource mana yang hilang) tanpa mengubah kontrak respons publik.
Bersikap ketat tentang apa yang Anda lampirkan:
- Publik (aman untuk klien): pesan singkat, kode stabil, dan kadang nama field untuk validasi.
- Privat (hanya log):
Erryang mendasari, info stack, error SQL mentah, token, data pengguna.
Pemecahan itu memungkinkan Anda membantu klien tanpa mengekspos internal.
Pemetaan tipe error ke status HTTP secara konsisten
Setelah Anda punya error bertipe, tugas berikutnya yang membosankan tapi penting: tipe error yang sama seharusnya selalu menghasilkan status HTTP yang sama. Klien akan membangun logika berdasarkan itu.
Pemetaan praktis yang bekerja untuk sebagian besar API:
| Error type (contoh) | Status | Kapan digunakan |
|---|---|---|
| BadRequest (JSON rusak, query param wajib hilang) | 400 | Request tidak valid pada level protokol atau format. |
| Unauthenticated (token tidak ada/invalid) | 401 | Klien perlu otentikasi. |
| Forbidden (tidak punya izin) | 403 | Auth valid, tapi akses tidak diizinkan. |
| NotFound (ID resource tidak ada) | 404 | Resource yang diminta tidak ada (atau Anda memilih menyembunyikan eksistensi). |
| Conflict (kontraint unik, mismatch versi) | 409 | Request terbentuk dengan benar, tapi bertabrakan dengan state saat ini. |
| ValidationFailed (aturan field) | 422 | Bentuknya benar, tapi validasi bisnis gagal (format email, panjang minimal). |
| RateLimited | 429 | Terlalu banyak permintaan dalam jendela waktu. |
| Internal (error tidak diketahui) | 500 | Bug atau kegagalan tak terduga. |
| Unavailable (dependency down, timeout, maintenance) | 503 | Masalah sisi server sementara. |
Dua pembedaan yang mencegah banyak kebingungan:
- 400 vs 422: gunakan 400 saat Anda tidak bisa menafsirkan request (JSON rusak, tipe salah). Gunakan 422 saat Anda bisa mem-parse-nya, tapi nilainya tidak bisa diterima.
- 409 vs 422: gunakan 422 untuk validasi level field (password terlalu pendek). Gunakan 409 ketika data valid tapi tidak bisa diterapkan karena state (email sudah dipakai, order sudah dikirim, kegagalan optimistic lock).
Panduan retry penting:
- Biasanya aman untuk retry: 503, dan kadang 429 (setelah menunggu).
- Biasanya tidak aman untuk retry tanpa perubahan: 400, 401, 403, 404, 409, 422.
- Jika operasi idempoten (PUT dengan body sama, atau POST dengan idempotency key), retry menjadi lebih aman bahkan setelah kegagalan sementara.
Request ID: cara tercepat debug masalah klien
Request ID adalah nilai unik singkat yang mengidentifikasi satu panggilan API secara end-to-end. Jika klien bisa melihatnya di setiap respons, dukungan menjadi sederhana: “Kirimkan request ID” seringkali cukup untuk menemukan log dan kegagalan yang tepat.
Kebiasaan ini bermanfaat untuk respons sukses maupun error.
Aturan pembuatan dan propagasi
Gunakan aturan jelas: jika klien mengirim request ID, pertahankan. Jika tidak, buat yang baru.
- Terima ID masuk dari satu nama header (pilih satu dan dokumentasikan, mis.
X-Request-Id). - Jika header hilang atau kosong, buat ID baru di edge (middleware) dan lampirkan ke context request.
- Jangan pernah mengganti ID di tengah request. Operkan ke downstream (DB, service lain) lewat context atau header.
Letakkan request ID di tiga tempat:
- Header respons (nama header yang sama yang Anda terima)
- Body respons (sebagai
request_iddalam skema standar Anda) - Logs (sebagai field terstruktur di setiap baris log)
Batch dan kerja asinkron
Untuk endpoint batch atau job background, pertahankan parent request ID. Contoh: klien mengunggah 200 baris, 12 gagal validasi, dan Anda mengantri pekerjaan. Kembalikan satu request_id untuk seluruh panggilan, dan sertakan parent_request_id pada setiap job dan setiap error per-item. Dengan begitu, Anda bisa menelusuri “satu upload” meski menyebar ke banyak tugas.
Logging dan metrik tanpa membocorkan internal
Klien butuh respons error yang jelas dan stabil. Log Anda butuh kebenaran yang berantakan. Pisahkan dua dunia itu: kembalikan pesan aman dan kode publik ke klien, sementara log menyimpan penyebab internal, stack, dan konteks di server.
Log satu event terstruktur untuk setiap respons error, dapat dicari berdasarkan request_id.
Field yang berguna untuk konsistensi:
- request_id
- user_id atau account_id (saat terautentikasi)
- kode error publik dan status HTTP
- nama handler/route dan method
- detail error internal (wrapped cause, error field validasi, timeout upstream)
Simpan detail internal hanya di log server (atau penyimpanan error internal). Klien tidak boleh melihat error database mentah, teks query, stack trace, atau pesan provider. Jika Anda menjalankan banyak service, field internal seperti source (api, db, auth, upstream) dapat mempercepat triase.
Amati endpoint yang berisik dan error yang dibatasi laju. Jika sebuah endpoint menghasilkan 429 atau 400 ribuan kali per menit, hindari spam log: sample event berulang, atau turunkan severity untuk error yang diharapkan sambil tetap menghitungnya di metrik.
Metrik menangkap masalah lebih awal daripada log. Lacak jumlah yang dikelompokkan berdasarkan status HTTP dan kode error, dan beri alert pada lonjakan tiba-tiba. Jika RATE_LIMITED naik 10x setelah deploy, Anda akan melihatnya cepat meski log di-sample.
Langkah demi langkah: terapkan pipeline error konsisten di Go
Cara termudah membuat error konsisten adalah berhenti menanganinya “di mana-mana” dan arahkan lewat satu pipeline kecil. Pipeline itu memutuskan apa yang dilihat klien dan apa yang Anda simpan di log.
Pipeline dalam 5 langkah praktis
Mulai dengan satu set kecil kode error yang bisa diandalkan klien (misal: INVALID_ARGUMENT, NOT_FOUND, UNAUTHORIZED, CONFLICT, INTERNAL). Bungkus mereka dalam error bertipe yang hanya mengekspos field publik aman (code, pesan aman, detail opsional seperti field yang salah). Simpan penyebab internal privat.
Kemudian implementasikan satu fungsi translator yang mengubah error apa pun menjadi (statusCode, responseBody). Di sinilah error bertipe dipetakan ke status HTTP, dan error tak dikenal menjadi respons 500 yang aman.
Selanjutnya, tambahkan middleware yang:
- memastikan setiap request punya
request_id - melakukan recover dari panic
Panic tidak boleh pernah menumpahkan stack trace ke klien. Kembalikan respons 500 biasa dengan pesan generik, dan log penuh panic dengan request_id yang sama.
Akhirnya, ubah handler Anda sehingga mereka mengembalikan error alih-alih menulis respons langsung. Satu wrapper dapat memanggil handler, menjalankan translator, dan menulis JSON dalam format standar.
Checklist ringkas:
- Definisikan error bertipe dengan field aman dan kode stabil.
- Translasi error ke status dan JSON respons di satu tempat.
- Tambahkan middleware request ID dan recovery panic.
- Buat handler mengembalikan error, bukan respons.
- Tambahkan tes golden untuk translator dan wrapper.
Tes golden penting karena mengunci kontrak. Jika seseorang nanti mengubah pesan atau status, tes gagal sebelum klien terkejut.
Contoh: satu endpoint, tiga kegagalan, respons yang dapat diprediksi
Bayangkan satu endpoint: aplikasi klien membuat record customer.
POST /v1/customers dengan JSON seperti { "email": "[email protected]", "name": "Pat" }. Server selalu mengembalikan bentuk error yang sama dan selalu menyertakan request_id.
1) Error validasi (400)
Email hilang atau formatnya salah. Klien bisa menandai field tersebut.
{
"request_id": "req_01HV9N2K6Q7A3W1J9K8B",
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields need attention.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
}
}
2) Konflik (409)
Email sudah ada. Klien dapat menyarankan masuk atau memilih email lain.
{
"request_id": "req_01HV9N3C2D0F0M3Q7Z9R",
"error": {
"code": "ALREADY_EXISTS",
"message": "A customer with this email already exists."
}
}
3) Kegagalan sementara (503)
Dependency sedang down. Klien bisa retry dengan backoff dan menampilkan pesan yang tenang.
{
"request_id": "req_01HV9N3X8P2J7T4N6C1D",
"error": {
"code": "TEMPORARILY_UNAVAILABLE",
"message": "We could not save your request right now. Please try again."
}
}
Dengan satu kontrak, klien bereaksi konsisten:
- 400: tandai field menggunakan
details.fields - 409: pandu pengguna ke langkah aman selanjutnya
- 503: minta retry dan tampilkan
request_idsebagai ID dukungan
Untuk dukungan, request_id yang sama adalah jalur tercepat ke penyebab nyata di log internal, tanpa mengekspos stack trace atau error database.
Perangkap umum yang memperburuk penanganan error
Cara tercepat mengganggu klien API adalah membuat mereka menebak. Jika satu endpoint mengembalikan { "error": "..." } dan yang lain mengembalikan { "message": "..." }, setiap klien berubah menjadi tumpukan kasus khusus, dan bug bersembunyi berminggu-minggu.
Beberapa kesalahan yang sering muncul:
- Mengembalikan HTTP 200 dengan error di dalam body, atau berpindah antar skema error di endpoint berbeda.
- Mengekspos internal di pesan pengguna, seperti error SQL, stack trace, IP, hostname dependency, atau path file.
- Menggunakan teks manusia sebagai satu-satunya pengenal, alih-alih
codestabil yang bisa dipakai mesin. - Mengubah kode error secara casual (atau menggunakan kode yang sama untuk masalah berbeda) sehingga memutus klien yang dibuat untuk perilaku lama.
- Menambahkan
request_idhanya pada kegagalan, sehingga Anda tidak bisa mengkorelasikan laporan pengguna dengan panggilan sukses yang memicu masalah kemudian.
Membocorkan internal adalah perangkap paling mudah. Sebuah handler mengembalikan err.Error() karena praktis, lalu nama constraint atau pesan pihak ketiga muncul di respons produksi. Jaga pesan klien aman dan singkat, dan letakkan penyebab detail di log.
Mengandalkan teks saja adalah masalah berkelanjutan. Jika klien harus mengurai kalimat bahasa Inggris seperti “email already exists,” Anda tidak bisa mengubah kata-kata tanpa merusak logika. Kode error stabil memungkinkan Anda menyesuaikan pesan, menerjemahkannya, dan menjaga perilaku konsisten.
Perlakukan kode error sebagai bagian kontrak publik Anda. Jika Anda harus mengubah satu, tambahkan kode baru dan biarkan kode lama tetap berfungsi untuk sementara, meski keduanya memetakan ke status HTTP yang sama.
Terakhir, sertakan field request_id yang sama di setiap respons, sukses atau gagal. Ketika pengguna berkata “awalnya berhasil, lalu rusak,” satu ID itu sering menyelamatkan satu jam menebak.
Daftar periksa cepat sebelum rilis
Sebelum rilis, lakukan pemeriksaan cepat untuk konsistensi:
- Satu bentuk error di mana-mana. Setiap endpoint mengembalikan field JSON yang sama (mis.
error.code,error.message,request_id). - Kode error stabil yang tercover. Buat kode singkat dan jelas (
VALIDATION_FAILED,NOT_FOUND,CONFLICT,UNAUTHORIZED). Tambahkan tes agar handler tidak mengembalikan kode tak dikenal secara tidak sengaja. - Satu aturan pemetaan status. Putuskan bagaimana tiap tipe error memetakan ke status HTTP, dan terapkan di satu tempat bersama.
- Request ID dua arah. Selalu kembalikan
request_iddan log itu untuk setiap request, termasuk panic dan timeout. - Pesan aman secara default. Pesan untuk pengguna harus singkat, jelas, dan dapat ditindaklanjuti; bukan stack trace, error SQL, atau nama vendor.
Setelah itu, cek beberapa endpoint secara manual. Pemicu error validasi, record hilang, dan kegagalan tak terduga. Jika respons berbeda antar endpoint (field berubah, status drift, pesan overshare), perbaiki pipeline bersama sebelum menambah fitur.
Aturan praktis: jika sebuah pesan membantu penyerang atau membingungkan pengguna normal, itu milik log, bukan respons.
Langkah berikutnya: standardisasi sekarang dan jaga konsistensi nanti
Tulis kontrak error yang Anda inginkan setiap endpoint ikuti, meski API Anda sudah hidup. Kontrak bersama (status, kode error stabil, pesan aman, dan request_id) adalah cara tercepat membuat error dapat diprediksi bagi klien.
Kemudian migrasikan secara bertahap. Pertahankan handler yang ada, tapi rute kegagalan mereka melalui satu mapper yang mengubah error internal menjadi bentuk respons publik Anda. Ini meningkatkan konsistensi tanpa rewrite besar yang berisiko, dan mencegah endpoint baru menciptakan format baru.
Jaga katalog kode error kecil dan perlakukan seperti bagian API. Saat seseorang ingin menambah kode baru, lakukan review singkat: apakah benar-benar baru, apakah penamaannya jelas, dan apakah memetakan ke status HTTP yang tepat?
Tambahkan beberapa tes yang menangkap pergeseran:
- Setiap respons error menyertakan
request_id. - Status code cocok dengan tipe error (bukan teks error).
error.codehadir dan berasal dari katalog.error.messagetetap aman dan tidak pernah memuat detail internal.- Error tak dikenal fallback ke 500 dengan pesan generik.
Jika Anda membangun backend Go dari nol, membantu mengunci kontrak sejak awal. Misalnya, Koder.ai (koder.ai) menyediakan mode perencanaan di mana Anda bisa mendefinisikan konvensi seperti skema error dan katalog kode sejak awal, lalu menjaga handler tetap selaras saat API berkembang.
Pertanyaan umum
What should a “consistent error response” look like?
Gunakan satu bentuk JSON untuk setiap respons error, di semua endpoint. Default praktis adalah request_id di tingkat atas plus objek error yang berisi code, message, dan details opsional sehingga client dapat mengurai dan menanggapi secara andal.
How do I avoid leaking internal details in API errors?
Kembalikan error.message sebagai kalimat singkat yang aman untuk pengguna dan simpan penyebab sebenarnya di log server. Jangan kembalikan error database mentah, stack trace, nama host internal, atau pesan dependency, walau saat pengembangan terasa membantu.
Do I really need an error code if I already have HTTP status codes?
Gunakan error.code yang stabil untuk logika mesin dan biarkan HTTP status menggambarkan kategori luas. Client harus membangun kondisi berdasarkan error.code (mis. ALREADY_EXISTS) dan menggunakan status sebagai panduan (mis. 409 berarti konflik state).
When should I use HTTP 400 vs 422?
Gunakan 400 ketika request tidak bisa diurai atau diinterpretasikan dengan andal (JSON rusak, tipe salah). Gunakan 422 ketika request ter-parse dengan benar namun melanggar aturan bisnis (format email tidak valid, password terlalu pendek).
When should I use HTTP 409 vs 422?
Gunakan 409 ketika input valid tetapi tidak bisa diterapkan karena konflik dengan state saat ini (email sudah dipakai, versi tidak cocok). Gunakan 422 untuk validasi level field di mana mengubah nilai memperbaiki masalah tanpa memerlukan perubahan state server.
How do typed errors in Go help keep responses consistent?
Buat satu set kecil error bertipe (validation, not found, conflict, unauthorized, internal) dan biarkan handler mengembalikan tipe-tipe ini. Lalu pakai satu translator bersama untuk memetakan tipe-tipe itu ke status HTTP dan bentuk respons JSON standar.
How should I generate and return request IDs?
Selalu kembalikan request_id di setiap respons, berhasil atau gagal, dan log id itu di setiap baris log server. Jika client melaporkan masalah, satu ID itu biasanya cukup untuk menemukan alur kegagalan di log.
Why is returning HTTP 200 with `{ "ok": false }` a bad idea?
Kembalikan 200 hanya saat operasi berhasil, dan gunakan 4xx/5xx untuk error. Menyembunyikan error di balik 200 memaksa client mengurai body dan menciptakan perilaku yang tidak konsisten antar endpoint.
Which errors should clients retry, and which should they not?
Jangan retry untuk 400, 401, 403, 404, 409, dan 422 karena retry tidak membantu tanpa perubahan. Izinkan retry untuk 503, dan kadang 429 setelah menunggu; jika Anda mendukung idempotency key, retry menjadi lebih aman untuk POST pada kegagalan sementara.
How do I prevent error responses from drifting as the API evolves?
Kunci kontrak dengan beberapa tes “golden” yang memeriksa status, error.code, dan keberadaan request_id. Tambahkan kode error baru tanpa mengubah makna lama, dan hanya tambahkan field opsional agar client lama tetap bekerja.