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:

StrategiBentuk umumKapan dipakai
Propagasireturn errpemanggil lebih tahu cara menangani; lapisan ini tak ada info baru
Bungkus + konteksfmt.Errorf("konteks: %w", err)menambah jejak lapisan tanpa memutus rantai
Retryulangi operasi beberapa kalikegagalan sementara: jaringan sibuk, rate limit
Gagal cepatlog.Fatal / os.Exit(1)lanjut tidak bermakna: konfigurasi wajib hilang saat start
Catat + lanjutlog error, proses item berikutnyabatch: satu item gagal tak boleh menjatuhkan sisanya
Fallbackpakai nilai cadangansumber 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 init tidak punya penanganan yang lebih baik daripada berhenti:
go
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.Errorf berkonteks, cukup %v saat mencetak.

Program Utuh #

Berkas strategi-demo.go menguji tiga strategi sekaligus — retry, fallback, dan dua mode batch:

go
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:

bash
go run strategi-demo.go

Output yang Dihasilkan:

text
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:

  1. Baris 16-22 (func (k *KlienCadangan) ambilData): objek ini mensimulasikan server yang menolak gagal kali pertama lalu pulih — sifat sementara inilah syarat utama retry.
  2. Baris 24-34 (func ambilDenganRetry): loop dibatasi maksPercobaan; keberhasilan langsung return, kegagalan total mengembalikan error yang membungkus ErrSibuk beserta jumlah percobaan.
  3. Baris 43-59 (func prosesBatch): satu fungsi melayani dua strategi lewat parameter berhentiPadaError; setiap kegagalan dicatat ke masalah agar laporan akhir tetap lengkap.
  4. Baris 51-54 (if berhentiPadaError ... continue): mode gagal cepat berhenti di kegagalan pertama; mode catat-dan-lanjut memakai continue sehingga item setelahnya tetap diproses.
  5. Baris 62-64 (KlienCadangan{gagal: 2}): dua kegagalan lalu sukses di percobaan ketiga — retry memulihkan operasi tanpa perlu fallback.
  6. Baris 66-70 (KlienCadangan{gagal: 5}): lima kegagalan melampaui batas tiga percobaan; pemanggil beralih ke nilai cadangan sehingga alur utama tetap berjalan.
  7. Baris 74-80 (prosesBatch dua 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

strategi-demo.go

Unduh berkas
go
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)
}