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

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.
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:
Sekumpulan aturan praktis:
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.
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:
details.fields sebagai peta field ke pesan.details.retry_after_seconds.details.docs_hint sebagai 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.
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:
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:
Err yang mendasari, info stack, error SQL mentah, token, data pengguna.Pemecahan itu memungkinkan Anda membantu klien tanpa mengekspos internal.
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:
Panduan retry penting:
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.
Gunakan aturan jelas: jika klien mengirim request ID, pertahankan. Jika tidak, buat yang baru.
X-Request-Id).Letakkan request ID di tiga tempat:
request_id dalam skema standar Anda)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.
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:
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.
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.
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:
request_idPanic 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:
Tes golden penting karena mengunci kontrak. Jika seseorang nanti mengubah pesan atau status, tes gagal sebelum klien terkejut.
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.
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"
}
}
}
}
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."
}
}
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:
details.fieldsrequest_id sebagai ID dukunganUntuk dukungan, request_id yang sama adalah jalur tercepat ke penyebab nyata di log internal, tanpa mengekspos stack trace atau error database.
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:
code stabil yang bisa dipakai mesin.request_id hanya 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.
Sebelum rilis, lakukan pemeriksaan cepat untuk konsistensi:
error.code, error.message, request_id).VALIDATION_FAILED, NOT_FOUND, CONFLICT, UNAUTHORIZED). Tambahkan tes agar handler tidak mengembalikan kode tak dikenal secara tidak sengaja.request_id dan log itu untuk setiap request, termasuk panic dan timeout.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.
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:
request_id.error.code hadir dan berasal dari katalog.error.message tetap aman dan tidak pernah memuat detail internal.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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.