Setelah error menempuh beberapa lapisan pembungkusan, pertanyaan pemanggil tidak lagi sederhana: “apakah kegagalan ini berakar pada ErrTerlaluPercobaan?” atau “apakah ada ErrorTransfer di suatu tempat di rantai ini?”. Dua fungsi package errors menjawabnya: errors.Is mencari keberadaan sebuah nilai, errors.As mencari dan mengambil sebuah tipe. Pada chapter ini kita akan membedah cara kerja keduanya dan membuktikan mengapa keduanya bukan sekadar type assertion.

Menelusuri Rantai dengan errors.Is #

Fungsi errors.Is(err, target) membandingkan setiap simpul rantai dengan target menggunakan operator ==, lalu turun ke penyebab berikutnya lewat Unwrap() hingga rantai habis. Pada setiap simpul, bila tipe error mengimplementasikan method Is(target error) bool, errors.Is memanggil method tersebut dan memakai hasilnya — standard library memakai mekanisme ini agar error syscall di dalam fs.PathError cocok dengan fs.ErrNotExist tanpa identitas pointer yang sama. Karena errors.Join membentuk rantai bercabang, pencarian errors.Is juga menjelajahi seluruh cabangnya.

Kapan pemanggil memakainya? Biasanya untuk keputusan bercabang: pengguna tidak ditemukan → balas 404; sibuk → coba lagi; selain itu → gagal umum.

Mengambil Tipe dengan errors.As #

errors.As(err, &target) menjelajah rantai dengan cara yang sama, tetapi yang dicocokkan adalah tipe. Argumen keduanya wajib pointer — pointer ke variabel bertipe pointer-ke-tipe-error:

go
var et *ErrorTransfer
if errors.As(err, &et) {
        fmt.Println(et.Rekening, et.Jumlah)
}

Bila satu simpul rantai bertipe *ErrorTransfer, fungsi tersebut mengisi et dengan simpul itu dan mengembalikan true. Tipe yang dicari harus mengimplementasikan error — bukan hanya konvensi, melainkan syarat yang diperiksa.

Bukan Sekadar Type Assertion #

Type assertion err.(*ErrorTransfer) memeriksa hanya tipe terluar dari err. Begitu error dibungkus fmt.Errorf, tipe terluarnya menjadi tipe pembungkus milik package fmt, sehingga assertion selalu gagal. errors.As bekerja berbeda: fungsi ini membuka setiap lapisan Unwrap() dan membandingkan tipe di kedalaman berapa pun. Perbedaan keduanya adalah perbedaan “melihat sampul” dan “membongkus seluruh kotak”.

Program Utuh #

Berkas errors-is-as-demo.go mensimulasikan dua lapisan transfer dengan dua jenis kegagalan berbeda:

go
package main

import (
	"errors"
	"fmt"
)

// ErrTerlaluPercobaan adalah sentinel untuk limit transaksi.
var ErrTerlaluPercobaan = errors.New("terlalu banyak percobaan")

// ErrorTransfer membawa data transaksi untuk keputusan bisnis.
type ErrorTransfer struct {
	Rekening string
	Jumlah   int
}

func (e *ErrorTransfer) Error() string {
	return fmt.Sprintf("transfer %s sebesar Rp%d gagal", e.Rekening, e.Jumlah)
}

func prosesTransfer(rekening string, jumlah int) error {
	if jumlah > 5_000_000 {
		return fmt.Errorf("validasi jumlah: %w", ErrTerlaluPercobaan)
	}
	if jumlah < 10_000 {
		return &ErrorTransfer{Rekening: rekening, Jumlah: jumlah}
	}
	return nil
}

func layananTransfer(rekening string, jumlah int) error {
	if err := prosesTransfer(rekening, jumlah); err != nil {
		return fmt.Errorf("layanan transfer: %w", err)
	}
	return nil
}

func main() {
	err := layananTransfer("BRI-001", 7_500_000)
	fmt.Println("A:", err)
	fmt.Println("   errors.Is ErrTerlaluPercobaan:", errors.Is(err, ErrTerlaluPercobaan))

	err = layananTransfer("BRI-001", 5_000)
	fmt.Println("B:", err)

	var et *ErrorTransfer
	if errors.As(err, &et) {
		fmt.Println("   errors.As -> rekening:", et.Rekening, "jumlah:", et.Jumlah)
	}

	_, ok := err.(*ErrorTransfer)
	fmt.Println("   type assertion langsung:", ok)
}

Jalankan program dari direktori chapter dengan perintah berikut:

bash
go run errors-is-as-demo.go

Output yang Dihasilkan:

text
A: layanan transfer: validasi jumlah: terlalu banyak percobaan
   errors.Is ErrTerlaluPercobaan: true
B: layanan transfer: transfer BRI-001 sebesar Rp5000 gagal
   errors.As -> rekening: BRI-001 jumlah: 5000
   type assertion langsung: false

Mari kita telaah baris demi baris program di atas:

  1. Baris 22-24 (if jumlah > 5_000_000 ...): lapisan bawah membungkus sentinel ErrTerlaluPercobaan dengan %w; kegagalan jenis ini identitasnya tetap dapat dicari dari luar.
  2. Baris 25-27 (if jumlah < 10_000 ...): jenis kegagalan kedua berupa *ErrorTransfer yang membawa data rekening dan jumlah untuk keputusan bisnis.
  3. Baris 31-36 (func layananTransfer): lapisan atas membungkus hasil lapisan bawah apa pun tipenya, sehingga kedua jenis kegagalan tiba di pemanggil dalam keadaan terbungkus.
  4. Baris 41 (errors.Is(err, ErrTerlaluPercobaan)): fungsi menuruni rantai layanan transfer → validasi jumlah → sentinel dan menemukan targetnya, meski tipe terluar bukan sentinel tersebut.
  5. Baris 46-49 (var et *ErrorTransfer; errors.As(err, &et)): target adalah pointer ke *ErrorTransfer; fungsi mengisi et dengan simpul bertipe itu sehingga field Rekening dan Jumlah terbaca langsung.
  6. Baris 51-52 (err.(*ErrorTransfer)): type assertion biasa gagal (false) karena tipe terluar err adalah pembungkus fmt, bukan *ErrorTransfer — bukti errors.As dan type assertion tidak saling menggantikan.

Dengan errors.Is dan errors.As, seluruh kegagalan yang bisa dipulihkan sudah terkelola sebagai nilai. Tetapi ada satu jalur kegagalan yang tidak lewat nilai balikan sama sekali: panic. Chapter berikutnya, panic dan recover , membahas kapan jalur itu sah dan bagaimana menangkapnya.

Referensi #

Berkas Sumber

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

errors-is-as-demo.go

Unduh berkas
go
package main

import (
	"errors"
	"fmt"
)

// ErrTerlaluPercobaan adalah sentinel untuk limit transaksi.
var ErrTerlaluPercobaan = errors.New("terlalu banyak percobaan")

// ErrorTransfer membawa data transaksi untuk keputusan bisnis.
type ErrorTransfer struct {
	Rekening string
	Jumlah   int
}

func (e *ErrorTransfer) Error() string {
	return fmt.Sprintf("transfer %s sebesar Rp%d gagal", e.Rekening, e.Jumlah)
}

func prosesTransfer(rekening string, jumlah int) error {
	if jumlah > 5_000_000 {
		return fmt.Errorf("validasi jumlah: %w", ErrTerlaluPercobaan)
	}
	if jumlah < 10_000 {
		return &ErrorTransfer{Rekening: rekening, Jumlah: jumlah}
	}
	return nil
}

func layananTransfer(rekening string, jumlah int) error {
	if err := prosesTransfer(rekening, jumlah); err != nil {
		return fmt.Errorf("layanan transfer: %w", err)
	}
	return nil
}

func main() {
	err := layananTransfer("BRI-001", 7_500_000)
	fmt.Println("A:", err)
	fmt.Println("   errors.Is ErrTerlaluPercobaan:", errors.Is(err, ErrTerlaluPercobaan))

	err = layananTransfer("BRI-001", 5_000)
	fmt.Println("B:", err)

	var et *ErrorTransfer
	if errors.As(err, &et) {
		fmt.Println("   errors.As -> rekening:", et.Rekening, "jumlah:", et.Jumlah)
	}

	_, ok := err.(*ErrorTransfer)
	fmt.Println("   type assertion langsung:", ok)
}