Menangani error bukan satu teknik tunggal, melainkan serangkaian keputusan: error ini diteruskan, ditambah konteks, dicoba ulang, atau menghentikan program? Rob Pike merangkum fondasinya dalam artikel Errors are values dengan satu kalimat yang sering dikutip: “Values can be programmed, and since errors are values, errors can be programmed.” Error adalah data, dan data bisa diproses. Chapter penutup section ini menyusun enam strategi penanganan, memetakan kapan masing-masing tepat, lalu mempraktikkannya dalam satu program.
Enam Strategi Penanganan #
Tabel berikut merangkum pilihan yang tersedia setiap kali err != nil:
| Strategi | Bentuk umum | Kapan dipakai |
|---|---|---|
| Propagasi | return err | pemanggil lebih tahu cara menangani; lapisan ini tak ada info baru |
| Bungkus + konteks | fmt.Errorf("konteks: %w", err) | menambah jejak lapisan tanpa memutus rantai |
| Retry | ulangi operasi beberapa kali | kegagalan sementara: jaringan sibuk, rate limit |
| Gagal cepat | log.Fatal / os.Exit(1) | lanjut tidak bermakna: konfigurasi wajib hilang saat start |
| Catat + lanjut | log error, proses item berikutnya | batch: satu item gagal tak boleh menjatuhkan sisanya |
| Fallback | pakai nilai cadangan | sumber utama tak tersedia tapi operasi tetap bisa dilanjutkan |
Tidak ada strategi yang selalu unggul — keputusannya bergantung pada tiga pertanyaan: apakah kegagalan itu sementara, apakah operasi aman diulang (idempotent), dan apakah kemajuan sebagian masih bernilai.
Menentukan Pilihan #
Pertanyaan sederhana berikut memotong keraguan saat error tiba di tangan kita:
- Sementara atau permanen? Error sementara (
ErrSibuk, koneksi reset) kandidat retry; error permanen (data tidak valid) cukup ditolak. - Aman diulang? Transfer dana yang belum idempotent tidak boleh asal diulang; baca data aman.
- Satu item atau seluruh proses? Batch import layak catat-dan-lanjut; migrasi database dengan data setengah jalan lebih aman gagal cepat.
- Terjadi saat start atau saat melayani? Kegagalan konfigurasi saat
inittidak punya penanganan yang lebih baik daripada berhenti:
func init() {
if os.Getenv("APP_ENV") == "" {
log.Fatal("APP_ENV wajib diisi sebelum aplikasi berjalan")
}
}Sentinel, Tipe Custom, atau Terbungkus #
Bentuk error yang kita pilih menentukan apa yang bisa dilakukan pemanggil. Ringkasannya:
- Pemanggil butuh cabang keputusan → sentinel +
errors.Is. - Pemanggil butuh data kegagalan → tipe custom +
errors.As. - Pemanggil hanya manusia membaca log →
fmt.Errorfberkonteks, cukup%vsaat mencetak.
Program Utuh #
Berkas strategi-demo.go menguji tiga strategi sekaligus — retry, fallback, dan dua mode batch:
package main
import (
"errors"
"fmt"
)
// ErrSibuk adalah sentinel untuk server yang menolak sementara.
var ErrSibuk = errors.New("server sedang sibuk")
// KlienCadangan mensimulasikan server yang gagal beberapa kali lalu pulih.
type KlienCadangan struct {
gagal int
}
func (k *KlienCadangan) ambilData() (string, error) {
if k.gagal > 0 {
k.gagal--
return "", fmt.Errorf("ambil data: %w", ErrSibuk)
}
return "data utama", nil
}
func ambilDenganRetry(k *KlienCadangan, maksPercobaan int) (string, error) {
for percobaan := 1; percobaan <= maksPercobaan; percobaan++ {
hasil, err := k.ambilData()
if err == nil {
fmt.Printf("percobaan %d: sukses\n", percobaan)
return hasil, nil
}
fmt.Printf("percobaan %d: %v\n", percobaan, err)
}
return "", fmt.Errorf("batal setelah %d percobaan: %w", maksPercobaan, ErrSibuk)
}
func prosesItem(nama string) error {
if len(nama) < 3 {
return fmt.Errorf("nama %q terlalu pendek", nama)
}
return nil
}
// prosesBatch membandingkan strategi gagal-cepat dengan catat-dan-lanjut.
func prosesBatch(daftar []string, berhentiPadaError bool) (int, []string) {
sukses := 0
var masalah []string
for _, nama := range daftar {
if err := prosesItem(nama); err != nil {
masalah = append(masalah, fmt.Sprintf("%q", nama))
fmt.Printf(" item %q gagal: %v\n", nama, err)
if berhentiPadaError {
return sukses, masalah
}
continue
}
sukses++
}
return sukses, masalah
}
func main() {
k := &KlienCadangan{gagal: 2}
hasil, err := ambilDenganRetry(k, 3)
fmt.Println("hasil retry:", hasil, "| err:", err)
k = &KlienCadangan{gagal: 5}
hasil, err = ambilDenganRetry(k, 3)
if err != nil {
fmt.Println("fallback dipakai: data cadangan")
}
daftar := []string{"buku", "pulpen", "ki", "penggaris", "ab"}
fmt.Println("mode gagal cepat:")
sukses, masalah := prosesBatch(daftar, true)
fmt.Printf(" selesai: %d sukses, masalah: %v\n", sukses, masalah)
fmt.Println("mode catat dan lanjut:")
sukses, masalah = prosesBatch(daftar, false)
fmt.Printf(" selesai: %d sukses, masalah: %v\n", sukses, masalah)
}Jalankan program dari direktori chapter dengan perintah berikut:
go run strategi-demo.goOutput yang Dihasilkan:
percobaan 1: ambil data: server sedang sibuk
percobaan 2: ambil data: server sedang sibuk
percobaan 3: sukses
hasil retry: data utama | err: <nil>
percobaan 1: ambil data: server sedang sibuk
percobaan 2: ambil data: server sedang sibuk
percobaan 3: ambil data: server sedang sibuk
fallback dipakai: data cadangan
mode gagal cepat:
item "ki" gagal: nama "ki" terlalu pendek
selesai: 2 sukses, masalah: ["ki"]
mode catat dan lanjut:
item "ki" gagal: nama "ki" terlalu pendek
item "ab" gagal: nama "ab" terlalu pendek
selesai: 3 sukses, masalah: ["ki" "ab"]Mari kita telaah baris demi baris program di atas:
- Baris 16-22 (
func (k *KlienCadangan) ambilData): objek ini mensimulasikan server yang menolakgagalkali pertama lalu pulih — sifat sementara inilah syarat utama retry. - Baris 24-34 (
func ambilDenganRetry): loop dibatasimaksPercobaan; keberhasilan langsungreturn, kegagalan total mengembalikan error yang membungkusErrSibukbeserta jumlah percobaan. - Baris 43-59 (
func prosesBatch): satu fungsi melayani dua strategi lewat parameterberhentiPadaError; setiap kegagalan dicatat kemasalahagar laporan akhir tetap lengkap. - Baris 51-54 (
if berhentiPadaError ... continue): mode gagal cepat berhenti di kegagalan pertama; mode catat-dan-lanjut memakaicontinuesehingga item setelahnya tetap diproses. - Baris 62-64 (
KlienCadangan{gagal: 2}): dua kegagalan lalu sukses di percobaan ketiga — retry memulihkan operasi tanpa perlu fallback. - Baris 66-70 (
KlienCadangan{gagal: 5}): lima kegagalan melampaui batas tiga percobaan; pemanggil beralih ke nilai cadangan sehingga alur utama tetap berjalan. - Baris 74-80 (
prosesBatchdua mode): mode gagal cepat selesai dengan 2 sukses, mode catat-dan-lanjut memproses semua item dan melaporkan 3 sukses beserta 2 masalah.
Section Error Handling ditutup di sini. Kita telah menempuh error sebagai interface, pembuatan dan pembungkusannya, pemeriksaan rantai, panic, hingga arsitektur batas lapisan — perangkat lengkap untuk menjaga kegagalan tetap terkelola. Perjalanan berikutnya berpindah ke organisasi kode: membagi program menjadi package dan module yang bisa dirawat, dibagikan, dan diuji — mulai dari section Package dan Module .
Referensi #
- Errors are values — artikel Rob Pike yang dikutip di pembuka chapter.
- Error handling and Go — ringkasan filosofi penanganan error Go.
- Effective Go — panduan gaya resmi termasuk pola error.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
strategi-demo.go
Unduh berkaspackage main
import (
"errors"
"fmt"
)
// ErrSibuk adalah sentinel untuk server yang menolak sementara.
var ErrSibuk = errors.New("server sedang sibuk")
// KlienCadangan mensimulasikan server yang gagal beberapa kali lalu pulih.
type KlienCadangan struct {
gagal int
}
func (k *KlienCadangan) ambilData() (string, error) {
if k.gagal > 0 {
k.gagal--
return "", fmt.Errorf("ambil data: %w", ErrSibuk)
}
return "data utama", nil
}
func ambilDenganRetry(k *KlienCadangan, maksPercobaan int) (string, error) {
for percobaan := 1; percobaan <= maksPercobaan; percobaan++ {
hasil, err := k.ambilData()
if err == nil {
fmt.Printf("percobaan %d: sukses\n", percobaan)
return hasil, nil
}
fmt.Printf("percobaan %d: %v\n", percobaan, err)
}
return "", fmt.Errorf("batal setelah %d percobaan: %w", maksPercobaan, ErrSibuk)
}
func prosesItem(nama string) error {
if len(nama) < 3 {
return fmt.Errorf("nama %q terlalu pendek", nama)
}
return nil
}
// prosesBatch membandingkan strategi gagal-cepat dengan catat-dan-lanjut.
func prosesBatch(daftar []string, berhentiPadaError bool) (int, []string) {
sukses := 0
var masalah []string
for _, nama := range daftar {
if err := prosesItem(nama); err != nil {
masalah = append(masalah, fmt.Sprintf("%q", nama))
fmt.Printf(" item %q gagal: %v\n", nama, err)
if berhentiPadaError {
return sukses, masalah
}
continue
}
sukses++
}
return sukses, masalah
}
func main() {
k := &KlienCadangan{gagal: 2}
hasil, err := ambilDenganRetry(k, 3)
fmt.Println("hasil retry:", hasil, "| err:", err)
k = &KlienCadangan{gagal: 5}
hasil, err = ambilDenganRetry(k, 3)
if err != nil {
fmt.Println("fallback dipakai: data cadangan")
}
daftar := []string{"buku", "pulpen", "ki", "penggaris", "ab"}
fmt.Println("mode gagal cepat:")
sukses, masalah := prosesBatch(daftar, true)
fmt.Printf(" selesai: %d sukses, masalah: %v\n", sukses, masalah)
fmt.Println("mode catat dan lanjut:")
sukses, masalah = prosesBatch(daftar, false)
fmt.Printf(" selesai: %d sukses, masalah: %v\n", sukses, masalah)
}