Eksepsi #
Go tidak punya try-catch. Bukan karena keterbatasan, tapi karena keputusan desain yang sangat deliberat: error adalah nilai biasa, bukan mekanisme kontrol aliran yang istimewa. Di Java atau Python, exception bisa muncul dari fungsi mana saja dan “melompati” banyak lapisan kode sebelum ditangani — ini membuat alur error sulit diprediksi dan dipahami. Di Go, error dikembalikan eksplisit sebagai return value, harus ditangani oleh pemanggil langsung, dan alurnya selalu bisa dibaca dari atas ke bawah. Hasilnya lebih verbose, tapi juga lebih transparan — tidak ada “surprise exception” yang mengejutkan.
Interface error — Fondasi Semua Error di Go
#
Semua error di Go mengimplementasikan interface yang sangat sederhana ini:
type error interface {
Error() string
}
Hanya satu method. Artinya, tipe apapun yang punya method Error() string adalah error yang valid di Go. Ini membuat sistem error Go sangat extensible — kamu bisa membuat error yang membawa data apapun, selama ia bisa mendeskripsikan dirinya sebagai string.
Cara Membuat Error #
errors.New — Error Sederhana
#
Untuk error yang hanya butuh pesan statis:
import "errors"
err := errors.New("file tidak ditemukan")
fmt.Println(err) // file tidak ditemukan
fmt.Println(err.Error()) // file tidak ditemukan — memanggil method Error()
Sentinel Errors — Error yang Bisa Dibandingkan #
Sentinel error adalah variabel package-level yang merepresentasikan kondisi error spesifik. Mereka memungkinkan pemanggil memeriksa jenis error dengan errors.Is:
package database
import "errors"
// Sentinel errors — nama selalu diawali Err
var (
ErrNotFound = errors.New("record tidak ditemukan")
ErrDuplicate = errors.New("record sudah ada")
ErrUnauthorized = errors.New("tidak memiliki akses")
ErrConnFailed = errors.New("koneksi database gagal")
)
func FindUser(id int) (*User, error) {
user := db.Query(id)
if user == nil {
return nil, ErrNotFound // kembalikan sentinel error
}
return user, nil
}
// Di pemanggil — bisa cek jenis error secara tepat
user, err := database.FindUser(42)
if errors.Is(err, database.ErrNotFound) {
// tangani kasus "tidak ada" secara spesifik
return nil, fmt.Errorf("user 42 tidak ada di sistem")
}
if err != nil {
// error lain yang tidak diharapkan
return nil, fmt.Errorf("gagal ambil user: %w", err)
}
fmt.Errorf — Error dengan Konteks
#
fmt.Errorf membuat error dengan pesan yang diformat. Gunakan %w (bukan %v) untuk membungkus error asli sehingga masih bisa ditelusuri:
import "fmt"
// %v — hanya menyertakan string error, chain PUTUS
err1 := fmt.Errorf("gagal membaca file: %v", originalErr)
// %w — membungkus error asli, chain TERJAGA
err2 := fmt.Errorf("gagal membaca file: %w", originalErr)
// Dengan %w, originalErr masih bisa ditemukan dengan errors.Is
errors.Is(err2, originalErr) // true
errors.Is(err1, originalErr) // false — chain sudah putus
Custom Error Types — Error yang Membawa Data #
Ketika error perlu membawa informasi lebih dari sekadar string, buat tipe error kustom dengan mengimplementasikan interface error:
// Error untuk validasi input — membawa field mana yang bermasalah
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validasi gagal pada field %q (nilai: %v): %s",
e.Field, e.Value, e.Message)
}
// Error untuk HTTP — membawa status code
type HTTPError struct {
StatusCode int
Status string
Body []byte
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d %s", e.StatusCode, e.Status)
}
// Error untuk operasi database — membawa query yang gagal
type DBError struct {
Op string // operasi: "insert", "select", "update"
Table string
Err error // error asli dari driver
}
func (e *DBError) Error() string {
return fmt.Sprintf("db %s pada tabel %q: %v", e.Op, e.Table, e.Err)
}
func (e *DBError) Unwrap() error {
return e.Err // wajib untuk errors.Is/As bekerja dengan chain
}
Menggunakan Custom Error Types #
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Message: "tidak boleh negatif",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "melebihi batas maksimum 150",
}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
// Ekstrak informasi detail dengan errors.As
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field %q gagal: %s\n", valErr.Field, valErr.Message)
}
}
}
Error Wrapping — Membangun Error Chain #
Error wrapping memungkinkan kamu menambahkan konteks ke error sambil mempertahankan error aslinya. Hasilnya adalah error chain — serangkaian error yang terhubung:
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// Wrap: tambahkan konteks "di mana", pertahankan "apa"
return nil, fmt.Errorf("readConfig(%q): %w", path, err)
}
return data, nil
}
func parseConfig(path string) (*Config, error) {
data, err := readConfig(path)
if err != nil {
return nil, fmt.Errorf("parseConfig: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parseConfig: unmarshal: %w", err)
}
return &cfg, nil
}
func startServer(configPath string) error {
cfg, err := parseConfig(configPath)
if err != nil {
return fmt.Errorf("startServer: %w", err)
}
// ...
return nil
}
Jika os.ReadFile gagal, error chain-nya akan terlihat seperti:
startServer: parseConfig: readConfig("/etc/app/config.json"): open /etc/app/config.json: no such file or directory
Setiap lapisan menambahkan konteks — sangat berguna untuk debugging di production.
errors.Is — Menelusuri Chain
#
errors.Is memeriksa apakah error tertentu ada di dalam chain, menelusuri sampai ke ujung:
var ErrPermission = errors.New("akses ditolak")
func readSecretFile() error {
return fmt.Errorf("readSecretFile: %w",
fmt.Errorf("sistem operasi: %w", ErrPermission))
}
func main() {
err := readSecretFile()
// errors.Is menelusuri seluruh chain
fmt.Println(errors.Is(err, ErrPermission)) // true — meski terbungkus dua kali
// Perbandingan langsung tidak menemukan karena sudah dibungkus
fmt.Println(err == ErrPermission) // false
}
Implementasi Is() Kustom
#
Untuk custom error type yang perlu logika pencocokan lebih kompleks dari kesamaan pointer:
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s dengan ID %d tidak ditemukan", e.Resource, e.ID)
}
// Implementasi Is() kustom — cocok berdasarkan Resource saja
func (e *NotFoundError) Is(target error) bool {
t, ok := target.(*NotFoundError)
if !ok {
return false
}
// Cocok jika Resource sama (abaikan ID)
return e.Resource == t.Resource || t.Resource == ""
}
func main() {
err := &NotFoundError{Resource: "User", ID: 42}
target := &NotFoundError{Resource: "User"} // ID kosong = wildcard
fmt.Println(errors.Is(err, target)) // true — Resource sama
}
errors.As — Ekstrak Tipe Error Tertentu
#
errors.As menelusuri chain dan mengekstrak error bertipe tertentu ke variabel target:
func processRequest(userID int) error {
user, err := db.FindUser(userID)
if err != nil {
return fmt.Errorf("processRequest: %w",
&DBError{Op: "select", Table: "users", Err: err})
}
_ = user
return nil
}
func main() {
err := processRequest(999)
// errors.As — ekstrak *DBError dari chain manapun
var dbErr *DBError
if errors.As(err, &dbErr) {
fmt.Printf("Operasi yang gagal: %s pada tabel %s\n",
dbErr.Op, dbErr.Table)
}
// errors.As juga menelusuri melalui wrapping
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("Validasi gagal:", valErr.Message)
} else {
fmt.Println("Bukan validation error")
}
}
Perbedaan errors.Is vs errors.As
#
errors.Is(err, target):
→ Memeriksa KESAMAAN nilai
→ target biasanya sentinel error (var ErrXxx = errors.New(...))
→ Menjawab: "apakah error ini adalah ErrNotFound?"
errors.As(err, &target):
→ Memeriksa KECOCOKAN TIPE dan mengekstrak nilainya
→ target adalah pointer ke tipe error konkret (*MyError)
→ Menjawab: "apakah ada *ValidationError dalam chain ini, dan jika ada, berikan ke saya"
Pola Error Handling Idiomatik #
Annotate at Boundary — Tambahkan Konteks di Batas Lapisan #
// ANTI-PATTERN: return error mentah tanpa konteks
func getUserPosts(userID int) ([]*Post, error) {
posts, err := db.Query("SELECT * FROM posts WHERE user_id = ?", userID)
if err != nil {
return nil, err // ✗ "sql: no rows" — tidak jelas konteksnya
}
return posts, nil
}
// BENAR: wrap dengan konteks di setiap boundary
func getUserPosts(userID int) ([]*Post, error) {
posts, err := db.Query("SELECT * FROM posts WHERE user_id = ?", userID)
if err != nil {
return nil, fmt.Errorf("getUserPosts(userID=%d): %w", userID, err)
// ✓ "getUserPosts(userID=42): sql: no rows" — jelas!
}
return posts, nil
}
Handle Once — Tangani Error Satu Kali #
// ANTI-PATTERN: log DAN return — error ditangani dua kali
func doWork() error {
if err := step1(); err != nil {
log.Println("step1 error:", err) // log di sini
return err // DAN return — double handling!
}
return nil
}
func main() {
if err := doWork(); err != nil {
log.Println("doWork error:", err) // log lagi di sini — duplikasi!
}
}
// BENAR: pilih satu — log ATAU return, tidak keduanya
func doWork() error {
if err := step1(); err != nil {
return fmt.Errorf("doWork.step1: %w", err) // hanya wrap dan return
}
return nil
}
func main() {
if err := doWork(); err != nil {
log.Println("Error:", err) // tangani SATU KALI di level tertinggi
}
}
Jangan Abaikan Error #
// ANTI-PATTERN: mengabaikan error
file, _ := os.Create("output.txt") // ✗ jika gagal, file adalah nil
file.Write([]byte("data")) // panic: nil pointer dereference
// BENAR: selalu tangani
file, err := os.Create("output.txt")
if err != nil {
return fmt.Errorf("gagal membuat file: %w", err)
}
defer file.Close()
panic dan recover
#
panic menghentikan eksekusi normal, menjalankan semua defer dalam stack, lalu crash. recover menangkap panic agar program tidak crash total — hanya valid dipanggil dalam defer.
Kapan panic Sah Digunakan
#
// 1. Bug pemrograman yang seharusnya tidak terjadi
func mustPositive(n int) int {
if n <= 0 {
panic(fmt.Sprintf("mustPositive: n harus > 0, got %d", n))
}
return n
}
// 2. Inisialisasi program yang gagal — tidak ada gunanya lanjut
func mustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(fmt.Sprintf("mustCompile: pattern tidak valid %q: %v", pattern, err))
}
return re
}
// Penggunaan di level package — panic terjadi sebelum main()
var emailRegex = mustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
recover untuk Mencegah Crash
#
// Middleware HTTP yang menangkap panic dari handler
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
// Tangkap panic, log, kembalikan 500
log.Printf("PANIC: %v\n%s", rec, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Safe wrapper — convert panic ke error
func safeCall(fn func() error) (err error) {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
err = fmt.Errorf("panic: %w", v)
default:
err = fmt.Errorf("panic: %v", v)
}
}
}()
return fn()
}
Kapan menggunakan panic vs error:
Gunakan ERROR (return value) untuk:
✓ Kondisi yang bisa diantisipasi: file tidak ada, input invalid
✓ Kegagalan I/O: network timeout, DB error
✓ Kegagalan bisnis: saldo tidak cukup, produk habis
Gunakan PANIC untuk:
✓ Bug pemrograman: index out of range, nil dereference
✓ Inisialisasi gagal: regex pattern salah, config wajib tidak ada
✓ Kondisi yang tidak mungkin terjadi jika kode benar
JANGAN pakai panic sebagai pengganti error handling biasa
Multi-Error — Menggabungkan Beberapa Error #
Sejak Go 1.20, errors.Join memungkinkan menggabungkan beberapa error menjadi satu:
import "errors"
func validateUser(u User) error {
var errs []error
if u.Name == "" {
errs = append(errs, errors.New("name tidak boleh kosong"))
}
if len(u.Name) > 100 {
errs = append(errs, errors.New("name terlalu panjang (max 100)"))
}
if u.Email == "" {
errs = append(errs, errors.New("email tidak boleh kosong"))
}
if !isValidEmail(u.Email) {
errs = append(errs, fmt.Errorf("email %q tidak valid", u.Email))
}
if u.Age < 0 || u.Age > 150 {
errs = append(errs, fmt.Errorf("age %d di luar rentang valid", u.Age))
}
return errors.Join(errs...) // nil jika errs kosong
}
func main() {
err := validateUser(User{Name: "", Email: "bukan-email", Age: -1})
if err != nil {
fmt.Println("Validasi gagal:")
// errors.Join menghasilkan error yang bisa di-unwrap satu per satu
for _, e := range errors.Unwrap(err).(interface{ Unwrap() []error }).Unwrap() {
fmt.Println(" -", e)
}
}
}
Contoh Program Lengkap #
Program berikut mensimulasikan layanan pembayaran dengan error handling berlapis yang realistis:
package main
import (
"errors"
"fmt"
"time"
)
// ── Sentinel Errors ───────────────────────────────────────────
var (
ErrInsufficientFunds = errors.New("saldo tidak mencukupi")
ErrAccountFrozen = errors.New("akun dibekukan")
ErrLimitExceeded = errors.New("melebihi limit transaksi")
ErrAccountNotFound = errors.New("akun tidak ditemukan")
)
// ── Custom Error Types ────────────────────────────────────────
type TransactionError struct {
Code string
AccountID string
Amount float64
Reason string
Err error
}
func (e *TransactionError) Error() string {
return fmt.Sprintf("[%s] transaksi gagal untuk akun %s (Rp%.0f): %s",
e.Code, e.AccountID, e.Amount, e.Reason)
}
func (e *TransactionError) Unwrap() error { return e.Err }
type AuditError struct {
TransactionID string
Err error
}
func (e *AuditError) Error() string {
return fmt.Sprintf("audit gagal untuk transaksi %s: %v", e.TransactionID, e.Err)
}
func (e *AuditError) Unwrap() error { return e.Err }
// ── Domain Models ─────────────────────────────────────────────
type Account struct {
ID string
Name string
Balance float64
DailyLimit float64
DailyUsed float64
Frozen bool
}
type Transaction struct {
ID string
From string
To string
Amount float64
Timestamp time.Time
Status string
}
// ── Repositories (simulasi) ───────────────────────────────────
type AccountRepo struct {
accounts map[string]*Account
}
func NewAccountRepo() *AccountRepo {
return &AccountRepo{
accounts: map[string]*Account{
"ACC001": {ID: "ACC001", Name: "Budi", Balance: 5_000_000, DailyLimit: 10_000_000, Frozen: false},
"ACC002": {ID: "ACC002", Name: "Sari", Balance: 2_000_000, DailyLimit: 5_000_000, Frozen: false},
"ACC003": {ID: "ACC003", Name: "Ahmad", Balance: 1_000_000, DailyLimit: 3_000_000, Frozen: true},
},
}
}
func (r *AccountRepo) FindByID(id string) (*Account, error) {
acc, ok := r.accounts[id]
if !ok {
return nil, fmt.Errorf("AccountRepo.FindByID(%q): %w", id, ErrAccountNotFound)
}
return acc, nil
}
func (r *AccountRepo) UpdateBalance(id string, newBalance float64) error {
acc, err := r.FindByID(id)
if err != nil {
return fmt.Errorf("AccountRepo.UpdateBalance: %w", err)
}
acc.Balance = newBalance
return nil
}
// ── Audit Service ─────────────────────────────────────────────
type AuditService struct {
logs []string
}
func (a *AuditService) Log(tx Transaction) error {
// Simulasi: gagal jika amount sangat besar (bug palsu)
if tx.Amount > 100_000_000 {
return fmt.Errorf("AuditService.Log: nilai terlalu besar untuk diaudit")
}
a.logs = append(a.logs, fmt.Sprintf("[%s] %s → %s: Rp%.0f (%s)",
tx.Timestamp.Format("15:04:05"),
tx.From, tx.To, tx.Amount, tx.Status))
return nil
}
// ── Payment Service ───────────────────────────────────────────
type PaymentService struct {
repo *AccountRepo
audit *AuditService
}
func NewPaymentService(repo *AccountRepo, audit *AuditService) *PaymentService {
return &PaymentService{repo: repo, audit: audit}
}
func (s *PaymentService) validateTransfer(from *Account, amount float64) error {
if from.Frozen {
return &TransactionError{
Code: "ACCOUNT_FROZEN",
AccountID: from.ID,
Amount: amount,
Reason: "akun pengirim dibekukan",
Err: ErrAccountFrozen,
}
}
if from.Balance < amount {
return &TransactionError{
Code: "INSUFFICIENT_FUNDS",
AccountID: from.ID,
Amount: amount,
Reason: fmt.Sprintf("saldo Rp%.0f < Rp%.0f", from.Balance, amount),
Err: ErrInsufficientFunds,
}
}
if from.DailyUsed+amount > from.DailyLimit {
remaining := from.DailyLimit - from.DailyUsed
return &TransactionError{
Code: "LIMIT_EXCEEDED",
AccountID: from.ID,
Amount: amount,
Reason: fmt.Sprintf("limit harian tersisa Rp%.0f", remaining),
Err: ErrLimitExceeded,
}
}
return nil
}
func (s *PaymentService) Transfer(fromID, toID string, amount float64) (*Transaction, error) {
// Ambil akun pengirim
from, err := s.repo.FindByID(fromID)
if err != nil {
return nil, fmt.Errorf("Transfer: %w", err)
}
// Ambil akun penerima
to, err := s.repo.FindByID(toID)
if err != nil {
return nil, fmt.Errorf("Transfer: %w", err)
}
// Validasi
if err := s.validateTransfer(from, amount); err != nil {
return nil, fmt.Errorf("Transfer: %w", err)
}
// Proses transfer
tx := &Transaction{
ID: fmt.Sprintf("TRX-%d", time.Now().UnixNano()),
From: fromID,
To: toID,
Amount: amount,
Timestamp: time.Now(),
Status: "SUCCESS",
}
from.Balance -= amount
from.DailyUsed += amount
to.Balance += amount
if err := s.repo.UpdateBalance(fromID, from.Balance); err != nil {
return nil, fmt.Errorf("Transfer: update pengirim: %w", err)
}
if err := s.repo.UpdateBalance(toID, to.Balance); err != nil {
return nil, fmt.Errorf("Transfer: update penerima: %w", err)
}
// Audit log — error di sini tidak membatalkan transfer
if err := s.audit.Log(*tx); err != nil {
// Log error audit tapi jangan gagalkan transaksi
fmt.Printf("⚠ Audit warning: %v\n", &AuditError{
TransactionID: tx.ID,
Err: err,
})
}
return tx, nil
}
// ── Helper untuk analisis error ───────────────────────────────
func describeError(err error) string {
if err == nil {
return "tidak ada error"
}
var txErr *TransactionError
if errors.As(err, &txErr) {
desc := fmt.Sprintf("TransactionError[%s]: %s", txErr.Code, txErr.Reason)
// Identifikasi sentinel error di dalam
switch {
case errors.Is(err, ErrInsufficientFunds):
desc += " (INSUFFICIENT_FUNDS)"
case errors.Is(err, ErrAccountFrozen):
desc += " (ACCOUNT_FROZEN)"
case errors.Is(err, ErrLimitExceeded):
desc += " (LIMIT_EXCEEDED)"
}
return desc
}
if errors.Is(err, ErrAccountNotFound) {
return "Akun tidak ditemukan"
}
return fmt.Sprintf("Error umum: %v", err)
}
func main() {
repo := NewAccountRepo()
audit := &AuditService{}
svc := NewPaymentService(repo, audit)
testCases := []struct {
name string
fromID string
toID string
amount float64
}{
{"Transfer normal", "ACC001", "ACC002", 1_000_000},
{"Saldo tidak cukup", "ACC002", "ACC001", 5_000_000},
{"Akun dibekukan", "ACC003", "ACC001", 500_000},
{"Akun tidak ditemukan", "ACC999", "ACC001", 100_000},
{"Transfer ke akun tidak ada", "ACC001", "ACC999", 100_000},
}
fmt.Println("=== Simulasi Transaksi Pembayaran ===\n")
for _, tc := range testCases {
fmt.Printf("▶ %s (Rp%.0f)\n", tc.name, tc.amount)
tx, err := svc.Transfer(tc.fromID, tc.toID, tc.amount)
if err != nil {
fmt.Printf(" ✗ Gagal: %v\n", err)
fmt.Printf(" ℹ Analisis: %s\n", describeError(err))
} else {
fmt.Printf(" ✓ Berhasil: ID=%s\n", tx.ID)
}
fmt.Println()
}
fmt.Println("=== Audit Log ===")
for _, log := range audit.logs {
fmt.Println(" ", log)
}
fmt.Println("\n=== Saldo Akhir ===")
for _, id := range []string{"ACC001", "ACC002", "ACC003"} {
acc, _ := repo.FindByID(id)
fmt.Printf(" %s (%s): Rp%.0f\n", acc.ID, acc.Name, acc.Balance)
}
}
Ringkasan #
- Error adalah nilai — dikembalikan sebagai return value terakhir, bukan mekanisme exception; alur selalu bisa dibaca linier.
errorinterface hanya butuhError() string— tipe apapun yang punya method ini adalah error yang valid.- Sentinel errors (
var ErrXxx = errors.New(...)) untuk kondisi yang perlu dibandingkan secara tepat denganerrors.Is.fmt.Errorfdengan%wuntuk membungkus error dengan konteks sambil mempertahankan chain;%vmemutus chain.- Custom error types ketika error perlu membawa data tambahan — implementasikan
Unwrap()agar chain tetap bisa ditelusuri.errors.Ismemeriksa kesamaan nilai menelusuri seluruh chain;errors.Asmengekstrak tipe tertentu dari chain.- Annotate at boundary — tambahkan konteks saat error melewati lapisan; handle once — tangani error di satu tempat saja.
panicuntuk bug pemrograman dan kegagalan inisialisasi;recoverdalamdeferuntuk mencegah crash di middleware.errors.Join(Go 1.20+) untuk menggabungkan beberapa error menjadi satu — berguna untuk validasi yang mengumpulkan semua error.- Jangan abaikan error dengan
_kecuali benar-benar yakin — selalu pertimbangkan apa yang terjadi jika operasi gagal.