Custom Errors — Go

Custom Errors di Go Selain errors.New dan fmt.Errorf, kamu bisa membuat tipe error sendiri untuk menyimpan informasi lebih detail. Custom Error Type type…

Custom error adalah tipe buatanmu sendiri yang bisa dipakai sebagai error, karena ia punya method Error() string. Bedanya dengan errors.New, tipe ini bisa membawa data. Bayangin kasir Warung Bakso Mas Bimo menolak pesanan. Nota "maaf, gagal" saja gak cukup: kasir butuh nota yang menulis menunya apa, minta berapa, dan sisa berapa, supaya bisa menawarkan sisanya. Lalu nota itu dimasukkan ke amplop bertuliskan "meja 4" sebelum diserahkan. Notanya custom error, amplopnya hasil wrapping, dan errors.Is/errors.As adalah cara membuka amplop untuk membaca nota di dalamnya.

🧪 Tebakan cepat: fmt.Errorf("meja %d: %v", 4, err) dan fmt.Errorf("meja %d: %w", 4, err) mencetak teks yang sama persis. Apakah errors.As masih bisa menemukan nota aslinya di dua-duanya? Tebak dulu, jawabannya ada di bagian "%w atau %v".

error itu interface dengan satu method

Di Go, error adalah interface bawaan:

type error interface {
	Error() string
}

Tipe apa pun yang punya method Error() string otomatis bisa dipakai sebagai error. Yang paling sederhana dibuat dengan errors.New, yang mengembalikan error berisi satu teks:

err := errors.New("file tidak ditemukan")
fmt.Println(err) // file tidak ditemukan

fmt.Println memanggil Error() untuk mendapat teksnya. Pesan error di Go ditulis huruf kecil tanpa titik di akhir, karena sering disambung dengan pesan lain.

Bikin tipe error sendiri

Stok di kasir Mas Bimo: bakso urat 2 porsi, mie ayam 5, es teh 10. Kalau pesanan melebihi stok, kita mau tahu menunya, jumlah yang diminta, dan sisanya:

type StokKurangError struct {
	Menu  string
	Minta int
	Sisa  int
}

func (e *StokKurangError) Error() string {
	return fmt.Sprintf("stok %s kurang: minta %d, sisa %d", e.Menu, e.Minta, e.Sisa)
}

var ErrMenuTidakAda = errors.New("menu tidak ada")

var stok = map[string]int{"bakso urat": 2, "mie ayam": 5, "es teh": 10}

func ambilStok(menu string, porsi int) error {
	sisa, ada := stok[menu]
	if !ada {
		return ErrMenuTidakAda
	}
	if porsi > sisa {
		return &StokKurangError{Menu: menu, Minta: porsi, Sisa: sisa}
	}
	stok[menu] = sisa - porsi
	return nil
}

Sentinel error: satu nilai untuk dibandingkan

ErrMenuTidakAda disebut sentinel error: satu nilai error yang dibuat sekali di level package, lalu dipakai pemanggil untuk mengecek "apakah ini error yang itu?". Namanya lazim diawali Err, dan diawali huruf besar kalau package lain perlu memakainya.

Tiap panggilan errors.New membuat nilai baru, walau teksnya sama. Jadi errors.New("menu tidak ada") == errors.New("menu tidak ada") bernilai false. Itu sebabnya sentinel disimpan di satu variabel dan dibandingkan dengan variabel itu.

Membungkus dengan %w

Fungsi di atasnya menambah konteks, yaitu nomor meja, tanpa membuang error aslinya:

func pesan(meja int, menu string, porsi int) error {
	err := ambilStok(menu, porsi)
	if err != nil {
		return fmt.Errorf("meja %d: %w", meja, err)
	}
	return nil
}

fmt.Errorf dengan verb %w membuat error baru yang teksnya meja 4: disambung teks error aslinya, dan menyimpan error aslinya di dalam. Error yang membungkus error lain membentuk rantai (chain): amplop "meja 4" berisi nota *StokKurangError. errors.Unwrap(err) mengambil isi satu lapis.

errors.Is dan errors.As

func main() {
	err := pesan(4, "bakso urat", 3)
	fmt.Println(err)
	fmt.Println("menu tidak ada?", errors.Is(err, ErrMenuTidakAda))

	var stokErr *StokKurangError
	if errors.As(err, &stokErr) {
		fmt.Println("tawarkan", stokErr.Sisa, "porsi saja")
	}
}
$ go run bakso.go
meja 4: stok bakso urat kurang: minta 3, sisa 2
menu tidak ada? false
tawarkan 2 porsi saja
Pesanan Yang tercetak errors.Is errors.As
bakso urat, 3 porsi meja 4: stok bakso urat kurang: minta 3, sisa 2 false true, Sisa 2
soto, 1 porsi meja 4: menu tidak ada true false
mie ayam, 2 porsi <nil> false false

%w atau %v

Ganti %w jadi %v, dan teks yang tercetak tetap meja 4: stok bakso urat kurang: minta 3, sisa 2. Tapi %v cuma menyalin teks error-nya. Error aslinya gak ikut disimpan, jadi errors.As mengembalikan false dan errors.Is juga false untuk soto. Jadi errors.As cuma menemukan nota aslinya di versi %w. Itu jawaban tebakannya.

Pakai %w kalau pemanggil boleh memeriksa error aslinya. Pakai %v kalau kamu sengaja menyembunyikannya, misalnya error dari database yang gak perlu diketahui pemanggil.

⚠️ Jebakan yang sering ketemu

TL;DR: error adalah interface dengan satu method, Error() string. errors.New("...") membuat error sederhana; tipe custom seperti StokKurangError{Menu, Minta, Sisa} dengan method Error() membawa data. Sentinel error seperti var ErrMenuTidakAda = errors.New("menu tidak ada") adalah satu nilai untuk dibandingkan. fmt.Errorf("meja %d: %w", meja, err) membungkus error dan menyimpan aslinya; %v cuma menyalin teksnya. errors.Is(err, ErrMenuTidakAda) mencari nilai sentinel di sepanjang rantai, errors.As(err, &stokErr) mencari tipe dan mengisi variabelnya. Jangan pakai == untuk error yang sudah dibungkus, kembalikan &StokKurangError{...} kalau receiver-nya pointer, dan tulis return nil di jalur yang berhasil.

Yang akan kamu pelajari