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)danfmt.Errorf("meja %d: %w", 4, err)mencetak teks yang sama persis. Apakaherrors.Asmasih 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
}
StokKurangErrorstruct biasa. Nama tipe error di Go lazim diakhiriError.func (e *StokKurangError) Error() stringmembuat*StokKurangErrormemenuhi interfaceerror. Pointer receiver, jadi yang dikembalikan harus pointer:&StokKurangError{...}.- Hasil
ambilStokbertipeerror, jadi pemanggilnya gak perlu tahu tipe aslinya untuk mencetak atau mengecekerr != nil. return nilberarti berhasil, dan stoknya dikurangi.
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
errors.Is(err, ErrMenuTidakAda)membuka rantai lapis demi lapis dan mencari nilai yang sama dengan sentinel itu. Untukpesan(5, "soto", 1)hasilnyatrue, padahalerr == ErrMenuTidakAdabernilaifalsekarenaerradalah amplopnya, bukan isinya.errors.As(err, &stokErr)mencari error yang tipenya*StokKurangErrordi sepanjang rantai. Kalau ketemu,stokErrdiisi dan hasilnyatrue, jadi field-nya bisa dibaca:stokErr.Sisa= 2.- Argumen kedua
errors.Asharus alamat variabel (&stokErr), dan tipe variabelnya harus tipe yang punya methodError, di sini*StokKurangError.
| 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
- Membandingkan dengan
==setelah dibungkus.err == ErrMenuTidakAdajadifalsebegitu ada lapisan%w. Selalu pakaierrors.Is. - Pointer receiver, tapi yang dikembalikan struct biasa.
return StokKurangError{...}ditolak compiler denganStokKurangError does not implement error (method Error has pointer receiver). Tulis&StokKurangError{...}. - Pointer nil bertipe.
var e *StokKurangErrorlalureturn emembuaterr != nilbernilaitruewalau isinya nil, karena interface-nya membawa tipe. Di jalur yang berhasil, tulisreturn nil. - Target
errors.Asyang salah.var e StokKurangErrorlaluerrors.As(err, &e)membuat program panic denganerrors: *target must be interface or implement error. Variabelnya harus*StokKurangError.
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.