← Semua pembelajaran / Go Nol → Enterprise
Fase 1 · Tipe, Interface & Error

Error sebagai nilai

Go tidak punya exception. Error adalah nilai biasa yang dikembalikan, dibungkus, dan diperiksa seperti data lain — dengan akibat bahwa setiap jalur kegagalan terlihat di kode.

Sumber asli go.dev Resmi Rangkuman ~9 menit baca

Intisari

  • error cuma interface satu method: Error() string.
  • Bungkus dengan %w di fmt.Errorf supaya konteks bertambah tanpa identitas errornya hilang.
  • errors.Is membandingkan identitas (sentinel); errors.As mengambil tipe konkretnya.
  • Bungkus sekali per lapisan, dengan konteks yang tidak diketahui pemanggil. Jangan bungkus lalu juga catat log di tempat yang sama.
  • Error sentinel domain (ErrTidakDitemukan) adalah yang dipetakan jadi status HTTP di lapisan terluar — bukan di dalam domain.

Bentuk dasarnya

type error interface {
	Error() string
}

Itu saja. Tiga cara membuatnya, dari yang paling sederhana:

// 1. Sekali pakai
return errors.New("koneksi gagal")
return fmt.Errorf("port %d sudah dipakai", port)

// 2. Sentinel — nilai tetap yang bisa dibandingkan pemanggil
var ErrTidakDitemukan = errors.New("tidak ditemukan")

// 3. Tipe error — saat pemanggil butuh DATA, bukan cuma identitas
type ErrValidasi struct {
	Field string
	Pesan string
}

func (e *ErrValidasi) Error() string {
	return fmt.Sprintf("%s: %s", e.Field, e.Pesan)
}

Membungkus dengan %w

func (p *Postgres) AmbilProduk(ctx context.Context, id int64) (Produk, error) {
	var pr Produk
	err := p.db.QueryRow(ctx, kueriAmbil, id).Scan(&pr.ID, &pr.Nama, &pr.Harga)
	if errors.Is(err, pgx.ErrNoRows) {
		// Terjemahkan error infrastruktur jadi error DOMAIN.
		return Produk{}, fmt.Errorf("produk %d: %w", id, ErrTidakDitemukan)
	}
	if err != nil {
		return Produk{}, fmt.Errorf("ambil produk %d: %w", id, err)
	}
	return pr, nil
}
Kata kerjaEfekKapan
%wMembungkus — errors.Is/As masih tembusHampir selalu
%vMenempel teksnya saja — rantainya terputusSaat kamu sengaja tidak mau membocorkan error dalam ke pemanggil

Isi pesan bungkus dengan konteks yang tidak diketahui lapisan dalam. Lapisan database tahu kueri apa yang gagal; ia tidak tahu pesanan siapa yang sedang diproses. Karena itu bungkusnya bertingkat: proses pesanan 991: ambil produk 42: connection refused. Rantai itulah yang membuat log satu baris cukup untuk mendiagnosis.

Yang tidak boleh: menulis fmt.Errorf("gagal: %w", err). Kata "gagal" tidak menambah informasi apa pun, dan setelah lima lapisan pesanmu jadi gagal: gagal: gagal: gagal: EOF.

Memeriksa: Is versus As

// errors.Is — "apakah di rantai ini ada error yang IDENTIK dengan X?"
if errors.Is(err, ErrTidakDitemukan) {
	http.Error(w, "tidak ditemukan", http.StatusNotFound)
	return
}

// errors.As — "apakah di rantai ini ada error bertipe T? kalau ya, isi ke variabel"
var ve *ErrValidasi
if errors.As(err, &ve) {
	http.Error(w, ve.Field+": "+ve.Pesan, http.StatusUnprocessableEntity)
	return
}

// errors.Join — beberapa kegagalan sekaligus (validasi banyak field)
err := errors.Join(errNama, errEmail)   // Is/As tetap bekerja untuk keduanya

Jangan pernah membandingkan pesan error dengan string. if err.Error() == "tidak ditemukan" akan rusak diam-diam begitu ada yang menambah konteks di lapisan mana pun. errors.Is ada justru untuk itu.

Perbandingan langsung err == ErrTidakDitemukan juga rapuh: ia gagal begitu errornya dibungkus. errors.Is menembus seluruh rantai.

Memetakan error domain jadi status HTTP

Ini pola yang dipakai sepanjang roadmap: domain tidak tahu apa itu HTTP, dan lapisan HTTP punya satu tempat yang menerjemahkan.

// internal/http/errors.go
func tulisError(w http.ResponseWriter, r *http.Request, err error) {
	var (
		status = http.StatusInternalServerError
		pesan  = "terjadi kesalahan"
		ve     *ErrValidasi
	)

	switch {
	case errors.Is(err, domain.ErrTidakDitemukan):
		status, pesan = http.StatusNotFound, "tidak ditemukan"
	case errors.Is(err, domain.ErrTidakBerhak):
		status, pesan = http.StatusForbidden, "tidak berhak"
	case errors.Is(err, domain.ErrKonflik):
		status, pesan = http.StatusConflict, "sudah ada"
	case errors.As(err, &ve):
		status, pesan = http.StatusUnprocessableEntity, ve.Error()
	case errors.Is(err, context.DeadlineExceeded):
		status, pesan = http.StatusGatewayTimeout, "waktu habis"
	}

	// Detail lengkap ke log, pesan aman ke pengguna.
	if status >= 500 {
		slog.ErrorContext(r.Context(), "permintaan gagal",
			"err", err, "path", r.URL.Path)
	}

	tulisJSON(w, status, map[string]string{"error": pesan})
}

Ini juga keputusan keamanan. Pesan error internal sering berisi nama tabel, kueri, alamat host, bahkan potongan data. Yang keluar ke pengguna harus pesan tetap; yang lengkap masuk log. Dibahas lagi di Fase 6.

Bungkus atau catat — pilih satu

LapisanYang dilakukan
Repository / klien luarBungkus dengan konteks teknis, kembalikan
Service / domainBungkus dengan konteks bisnis, atau terjemahkan jadi error domain
Handler HTTP / workerCatat sekali, lalu ubah jadi respons

Gejala "log berulang": satu kegagalan muncul lima kali di CloudWatch dengan pesan yang mirip. Penyebabnya selalu sama — setiap lapisan mencatat dan mengembalikan error yang sama. Aturannya: yang mengembalikan tidak mencatat; yang mencatat tidak mengembalikan.

Kapan boleh mengabaikan error

defer f.Close()               // baca-saja: tidak ada yang bisa dilakukan
_ = json.NewEncoder(w).Encode(v)  // klien sudah putus; sengaja diabaikan

// TAPI: pada berkas yang DITULIS, Close() bisa mengembalikan error
// dari flush terakhir — di sana error itu penting.
if err := f.Close(); err != nil {
	return fmt.Errorf("tutup %s: %w", nama, err)
}

Linter errcheck (bagian dari golangci-lint) memaksa setiap pengabaian ditulis eksplisit dengan _ =. Itu bukan formalitas — ia mengubah "lupa" jadi "keputusan yang terlihat di review".

Latihan: buat sentinel ErrTidakDitemukan di paket domain, kembalikan dari fungsi repository palsu, bungkus dua kali di lapisan berbeda dengan %w, lalu buktikan errors.Is masih mengenalinya. Setelah itu ganti satu %w jadi %v dan lihat errors.Is berhenti bekerja — itu selisih satu huruf yang mematikan seluruh penanganan errormu.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.