Errors #

Error handling adalah salah satu aspek yang paling membedakan Go dari bahasa lain. Alih-alih exception yang dilempar dan ditangkap di mana saja, Go memilih pendekatan eksplisit: setiap fungsi yang bisa gagal mengembalikan error sebagai nilai biasa, dan pemanggil bertanggung jawab memeriksa dan menanganinya. Package errors — bersama dengan fmt.Errorf — adalah fondasi dari sistem ini. Sejak Go 1.13, package ini diperkuat dengan kemampuan error wrapping: membungkus error asli dalam error baru yang menambahkan konteks, sambil tetap mempertahankan kemampuan untuk memeriksa jenis error asli menggunakan errors.Is dan errors.As. Memahami package errors dengan baik berarti memahami cara membangun sistem yang memberikan pesan error yang informatif, mudah di-debug, dan bisa ditangani secara berbeda berdasarkan jenisnya.

Filosofi Error di Go #

Sebelum masuk ke API, penting memahami mengapa Go memilih pendekatan ini:

flowchart LR
    subgraph Exception["Bahasa dengan Exception\n(Java, Python, C++)"]
        E1["fungsi() throws Exception"] --> E2["...banyak layer..."]
        E2 --> E3["catch (Exception e)"]
        E3 --> E4["Siapa yang throw?\nDari layer mana?\nKonteks apa?"]
    end

    subgraph Go["Go — Error sebagai Nilai"]
        G1["err := fungsi()"] --> G2{"err != nil?"}
        G2 -- Ya --> G3["Tangani di sini\natau wrap dan return"]
        G2 -- Tidak --> G4["Lanjutkan"]
        G3 --> G5["errors.Is / errors.As\nuntuk inspeksi jenis"]
    end

    style Exception fill:#fce4ec
    style Go fill:#e8f5e9

Keuntungan pendekatan Go:

  • Error selalu terlihat di signature fungsi — tidak ada kejutan tersembunyi
  • Setiap layer bisa menambahkan konteks sebelum meneruskan error
  • Pemanggil bisa memilih: tangani, wrap, atau abaikan (dengan _, tapi ini anti-pattern)
  • Error adalah nilai biasa — bisa disimpan, dibandingkan, diinspeksi

errors.New — Membuat Error Sederhana #

errors.New membuat error dengan pesan tetap. Ia adalah cara paling dasar untuk mendefinisikan error sentinel — error yang mewakili kondisi tertentu dan bisa dibandingkan dengan errors.Is.

package main

import (
    "errors"
    "fmt"
)

// Sentinel errors — variabel error yang didefinisikan di level package
// Konvensi: nama dimulai dengan "Err"
var (
    ErrTidakDitemukan  = errors.New("tidak ditemukan")
    ErrTidakDiizinkan  = errors.New("tidak diizinkan")
    ErrInputTidakValid = errors.New("input tidak valid")
    ErrKoneksiBermasalah = errors.New("koneksi bermasalah")
)

func cariPengguna(id int) (*Pengguna, error) {
    if id <= 0 {
        return nil, ErrInputTidakValid
    }
    if id > 1000 {
        return nil, ErrTidakDitemukan
    }
    return &Pengguna{ID: id, Nama: "Budi"}, nil
}

func main() {
    _, err := cariPengguna(-1)

    // Bandingkan dengan == — ANTI-PATTERN untuk error yang di-wrap
    if err == ErrInputTidakValid {
        fmt.Println("input salah") // bekerja tapi tidak robust
    }

    // BENAR: gunakan errors.Is — bekerja bahkan jika error di-wrap
    if errors.Is(err, ErrInputTidakValid) {
        fmt.Println("input salah") // selalu benar
    }
}

Mengapa errors.Is, Bukan == #

flowchart TD
    subgraph Tanpa["err == ErrTidakDitemukan"]
        T1["cariPengguna: ErrTidakDitemukan"] --> T2["service: wrap dengan fmt.Errorf %w"]
        T2 --> T3["handler: err == ErrTidakDitemukan"]
        T3 --> T4["false ❌\nerror sudah dibungkus,\ntidak sama persis"]
    end

    subgraph Dengan["errors.Is(err, ErrTidakDitemukan)"]
        D1["cariPengguna: ErrTidakDitemukan"] --> D2["service: wrap dengan fmt.Errorf %w"]
        D2 --> D3["handler: errors.Is(err, ErrTidakDitemukan)"]
        D3 --> D4["true ✓\nerrors.Is membuka semua lapisan\nwrapping secara rekursif"]
    end

    style T4 fill:#fce4ec
    style D4 fill:#e8f5e9
var ErrTidakDitemukan = errors.New("tidak ditemukan")

// Layer repository
func repoCariBuku(id int) error {
    return ErrTidakDitemukan
}

// Layer service — menambahkan konteks
func serviceCariBuku(id int) error {
    if err := repoCariBuku(id); err != nil {
        return fmt.Errorf("serviceCariBuku %d: %w", id, err)
        // Error sekarang: "serviceCariBuku 42: tidak ditemukan"
    }
    return nil
}

// Layer handler
func handlerCariBuku(id int) {
    err := serviceCariBuku(id)

    // == tidak bekerja karena error sudah dibungkus
    fmt.Println(err == ErrTidakDitemukan) // false

    // errors.Is membuka semua lapisan wrapping
    fmt.Println(errors.Is(err, ErrTidakDitemukan)) // true ✓
}

fmt.Errorf dengan %w — Error Wrapping #

fmt.Errorf dengan verb %w adalah cara idiomatis untuk membungkus error sambil menambahkan konteks. Ini berbeda dari %v yang hanya mengonversi error ke string tanpa kemampuan unwrap.

// Perbandingan %v vs %w
var ErrDB = errors.New("database error")

// Dengan %v — error lama jadi string, tidak bisa di-unwrap
err1 := fmt.Errorf("operasi gagal: %v", ErrDB)
fmt.Println(errors.Is(err1, ErrDB)) // false ❌

// Dengan %w — error lama terbungkus, bisa di-unwrap
err2 := fmt.Errorf("operasi gagal: %w", ErrDB)
fmt.Println(errors.Is(err2, ErrDB)) // true ✓

// Konvensi penamaan dalam pesan error
// Format: "namaFungsi argumen: pesan" atau "namaFungsi: pesan"
func muatKonfigurasi(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        // Sertakan path sebagai konteks — sangat berguna saat debug
        return fmt.Errorf("muatKonfigurasi %s: %w", path, err)
    }
    _ = data
    return nil
}

func inisialisasiApp(configPath string) error {
    if err := muatKonfigurasi(configPath); err != nil {
        return fmt.Errorf("inisialisasiApp: %w", err)
    }
    return nil
}

// Error yang terbentuk saat dipanggil dengan path yang salah:
// "inisialisasiApp: muatKonfigurasi /etc/app.yaml:
//  open /etc/app.yaml: no such file or directory"

Multiple Wrapping (Go 1.20+) #

Sejak Go 1.20, satu error bisa membungkus beberapa error sekaligus:

var (
    ErrValidasi = errors.New("validasi gagal")
    ErrDB       = errors.New("database error")
)

// Wrap dua error sekaligus dengan dua %w
err := fmt.Errorf("simpanData: %w dan %w", ErrValidasi, ErrDB)
fmt.Println(err)
// simpanData: validasi gagal dan database error

fmt.Println(errors.Is(err, ErrValidasi)) // true
fmt.Println(errors.Is(err, ErrDB))       // true

// errors.Join — cara lain untuk menggabungkan beberapa error (Go 1.20+)
errs := []error{ErrValidasi, ErrDB}
gabungan := errors.Join(errs...)
fmt.Println(gabungan)
// validasi gagal
// database error

fmt.Println(errors.Is(gabungan, ErrValidasi)) // true
fmt.Println(errors.Is(gabungan, ErrDB))       // true

errors.Is — Memeriksa Jenis Error #

errors.Is memeriksa apakah sebuah error — atau error manapun dalam rantai wrapping-nya — cocok dengan target yang diberikan.

flowchart TD
    Call["errors.Is(err, target)"] --> Step1["Periksa: err == target?"]
    Step1 -- Ya --> True["return true"]
    Step1 -- Tidak --> Step2{"err punya\nmethod Is(error) bool?"}
    Step2 -- Ya --> Step3["Panggil err.Is(target)"]
    Step3 -- true --> True
    Step3 -- false --> Step4{"err punya\nmethod Unwrap() error?"}
    Step2 -- Tidak --> Step4
    Step4 -- Ya --> Step5["err = err.Unwrap()\nulang dari awal"]
    Step4 -- Tidak --> Step6{"err punya method\nUnwrap() []error?"}
    Step6 -- Ya --> Step7["Periksa setiap error\ndalam slice"]
    Step6 -- Tidak --> False["return false"]
    Step7 --> True
    Step7 --> False

    style True fill:#e8f5e9
    style False fill:#fce4ec
import (
    "errors"
    "io"
    "io/fs"
    "os"
)

// Contoh dengan berbagai jenis error dari stdlib
func contohErrorsIs() {
    // Error dari os package
    _, err := os.Open("tidak-ada.txt")

    // Periksa dengan sentinel error dari io/fs
    if errors.Is(err, fs.ErrNotExist) {
        fmt.Println("file tidak ada")
    }
    if errors.Is(err, fs.ErrPermission) {
        fmt.Println("tidak ada izin")
    }

    // EOF — sentinel error dari io
    _, err2 := fmt.Sscan("", new(int))
    if errors.Is(err2, io.EOF) {
        fmt.Println("tidak ada data")
    }

    // errors.Is juga bekerja dengan nil
    var errNil error = nil
    fmt.Println(errors.Is(errNil, nil)) // true
}

// Custom Is method — untuk perbandingan yang lebih fleksibel
type ErrHTTP struct {
    StatusCode int
    Pesan      string
}

func (e *ErrHTTP) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Pesan)
}

// Implementasikan Is agar errors.Is bekerja berdasarkan status code
func (e *ErrHTTP) Is(target error) bool {
    t, ok := target.(*ErrHTTP)
    if !ok {
        return false
    }
    // Cocok jika status code sama (abaikan pesan)
    return e.StatusCode == t.StatusCode
}

var ErrNotFound = &ErrHTTP{StatusCode: 404}
var ErrUnauthorized = &ErrHTTP{StatusCode: 401}

func periksaResponse(err error) {
    if errors.Is(err, ErrNotFound) {
        fmt.Println("resource tidak ditemukan")
    }
    if errors.Is(err, ErrUnauthorized) {
        fmt.Println("perlu autentikasi")
    }
}

errors.As — Mengekstrak Tipe Error #

errors.As mencari dalam rantai error untuk error dengan tipe tertentu, dan mengekstrak nilainya ke variabel target. Ini adalah cara untuk mengakses field atau method dari custom error type.

// errors.As memungkinkan akses ke detail error
type ErrValidasi struct {
    Field   string
    Pesan   string
    Nilai   any
}

func (e *ErrValidasi) Error() string {
    return fmt.Sprintf("validasi gagal untuk field %q: %s (nilai: %v)",
        e.Field, e.Pesan, e.Nilai)
}

func validasiUmur(umur int) error {
    if umur < 0 {
        return &ErrValidasi{
            Field: "umur",
            Pesan: "tidak boleh negatif",
            Nilai: umur,
        }
    }
    if umur > 150 {
        return &ErrValidasi{
            Field: "umur",
            Pesan: "tidak realistis",
            Nilai: umur,
        }
    }
    return nil
}

func prosesRegistrasi(umur int) error {
    if err := validasiUmur(umur); err != nil {
        return fmt.Errorf("prosesRegistrasi: %w", err)
    }
    return nil
}

func main() {
    err := prosesRegistrasi(-5)

    // errors.Is tidak cukup — kita butuh detail field dan nilai
    // errors.As mengekstrak *ErrValidasi dari dalam rantai wrapping
    var errVal *ErrValidasi
    if errors.As(err, &errVal) {
        // Sekarang bisa akses field dari ErrValidasi
        fmt.Printf("Field bermasalah: %s\n", errVal.Field)
        fmt.Printf("Pesan: %s\n", errVal.Pesan)
        fmt.Printf("Nilai yang dikirim: %v\n", errVal.Nilai)

        // Bisa membuat response HTTP yang tepat
        // http.Error(w, errVal.Pesan, http.StatusBadRequest)
    }
}

errors.Is vs errors.As — Kapan Masing-masing #

flowchart TD
    Q{"Apa yang ingin\ndiperiksa?"} --> V["Apakah error adalah\nkondisi tertentu?\n(tidak butuh detail)"]
    Q --> D["Apakah error memiliki\ntipe tertentu dan\nbutuh mengakses field-nya?"]

    V --> Is["errors.Is(err, ErrTarget)\n\nContoh:\nerrors.Is(err, ErrTidakDitemukan)\nerrors.Is(err, io.EOF)\nerrors.Is(err, fs.ErrPermission)"]

    D --> As["errors.As(err, &target)\n\nContoh:\nerrors.As(err, &errValidasi)\nerrors.As(err, &errHTTP)\nerrors.As(err, &errDB)"]

    Is --> IsEx["Gunakan untuk:\n- Sentinel errors\n- Kondisi biner (ada/tidak)\n- Alur kontrol berdasarkan jenis error"]

    As --> AsEx["Gunakan untuk:\n- Custom error dengan data tambahan\n- Mengakses status code, field name, dll\n- Response yang disesuaikan per detail error"]

    style Is fill:#e3f2fd
    style As fill:#e8f5e9
// Contoh nyata: HTTP handler dengan error handling lengkap
func handlerCariBuku(w http.ResponseWriter, r *http.Request) {
    idStr := r.URL.Query().Get("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "ID tidak valid", http.StatusBadRequest)
        return
    }

    buku, err := serviceCariBuku(id)
    if err != nil {
        // Periksa jenis error untuk response yang tepat
        switch {
        case errors.Is(err, ErrTidakDitemukan):
            http.Error(w, "Buku tidak ditemukan", http.StatusNotFound)

        case errors.Is(err, ErrTidakDiizinkan):
            http.Error(w, "Akses ditolak", http.StatusForbidden)

        default:
            // Periksa apakah ada detail error validasi
            var errVal *ErrValidasi
            if errors.As(err, &errVal) {
                http.Error(w,
                    fmt.Sprintf("Input tidak valid: %s", errVal.Pesan),
                    http.StatusBadRequest)
                return
            }

            // Error yang tidak dikenal — log dan return 500
            log.Printf("error tidak terduga: %v", err)
            http.Error(w, "Terjadi kesalahan internal", http.StatusInternalServerError)
        }
        return
    }

    json.NewEncoder(w).Encode(buku)
}

errors.Unwrap — Membuka Satu Lapisan #

errors.Unwrap membuka satu lapisan wrapping — berguna jika kamu ingin menelusuri rantai error secara manual.

err1 := errors.New("error dasar")
err2 := fmt.Errorf("lapisan 2: %w", err1)
err3 := fmt.Errorf("lapisan 3: %w", err2)

fmt.Println(err3)                    // lapisan 3: lapisan 2: error dasar
fmt.Println(errors.Unwrap(err3))     // lapisan 2: error dasar
fmt.Println(errors.Unwrap(errors.Unwrap(err3))) // error dasar
fmt.Println(errors.Unwrap(err1))     // nil — tidak ada lapisan lagi

// Menelusuri seluruh rantai error secara manual
func telusuriError(err error) {
    fmt.Println("=== Rantai Error ===")
    for err != nil {
        fmt.Printf("  %T: %v\n", err, err)
        err = errors.Unwrap(err)
    }
}

telusuriError(err3)
// === Rantai Error ===
//   *fmt.wrapError: lapisan 3: lapisan 2: error dasar
//   *fmt.wrapError: lapisan 2: error dasar
//   *errors.errorString: error dasar

errors.Join — Menggabungkan Beberapa Error (Go 1.20+) #

errors.Join berguna saat melakukan validasi yang menghasilkan banyak error sekaligus, atau saat menjalankan beberapa operasi parallel yang masing-masing bisa gagal.

import "errors"

// Validasi form — kumpulkan semua error sekaligus
type FormRegistrasi struct {
    Nama     string
    Email    string
    Password string
    Umur     int
}

func validasiForm(form FormRegistrasi) error {
    var errs []error

    if form.Nama == "" {
        errs = append(errs, errors.New("nama tidak boleh kosong"))
    } else if len(form.Nama) < 2 {
        errs = append(errs, errors.New("nama minimal 2 karakter"))
    }

    if form.Email == "" {
        errs = append(errs, errors.New("email tidak boleh kosong"))
    } else if !strings.Contains(form.Email, "@") {
        errs = append(errs, errors.New("format email tidak valid"))
    }

    if len(form.Password) < 8 {
        errs = append(errs, errors.New("password minimal 8 karakter"))
    }

    if form.Umur < 18 {
        errs = append(errs, errors.New("umur minimal 18 tahun"))
    }

    // errors.Join menggabungkan semua error
    // mengembalikan nil jika slice kosong
    return errors.Join(errs...)
}

func main() {
    form := FormRegistrasi{
        Nama:     "A",
        Email:    "bukan-email",
        Password: "123",
        Umur:     15,
    }

    if err := validasiForm(form); err != nil {
        fmt.Println("Validasi gagal:")
        fmt.Println(err)
        // Validasi gagal:
        // nama minimal 2 karakter
        // format email tidak valid
        // password minimal 8 karakter
        // umur minimal 18 tahun
    }
}

Menjalankan Operasi Paralel dengan errors.Join #

import (
    "errors"
    "sync"
)

func jalankanParalel(tugas []func() error) error {
    var (
        mu   sync.Mutex
        errs []error
        wg   sync.WaitGroup
    )

    for _, t := range tugas {
        wg.Add(1)
        go func(fn func() error) {
            defer wg.Done()
            if err := fn(); err != nil {
                mu.Lock()
                errs = append(errs, err)
                mu.Unlock()
            }
        }(t)
    }

    wg.Wait()
    return errors.Join(errs...)
}

// Penggunaan
err := jalankanParalel([]func() error{
    func() error { return kirimEmail("[email protected]") },
    func() error { return kirimNotifikasi(123) },
    func() error { return updateCache("key") },
})

if err != nil {
    fmt.Println("Beberapa operasi gagal:", err)
}

Custom Error Type #

Untuk kasus yang lebih kompleks dari sentinel error, kamu bisa mendefinisikan tipe error sendiri dengan mengimplementasikan interface error.

flowchart TD
    Interface["interface error {\n  Error() string\n}"] --> Basic["errors.New\nPesan tetap, tidak ada data"]
    Interface --> Fmt["fmt.Errorf\nPesan dinamis, bisa wrap"]
    Interface --> Custom["Custom struct\nData tambahan, method kustom"]

    Custom --> C1["ErrValidasi\n{Field, Pesan, Nilai}"]
    Custom --> C2["ErrHTTP\n{StatusCode, Pesan}"]
    Custom --> C3["ErrDB\n{Query, Params, Underlying}"]
    Custom --> C4["ErrTimeout\n{Operasi, Durasi}"]

    C1 --> When1["Butuh tahu\nfield mana yang gagal"]
    C2 --> When2["Butuh tahu\nHTTP status code"]
    C3 --> When3["Butuh tahu\nquery yang gagal"]
    C4 --> When4["Butuh tahu\nberapa lama timeout"]

    style Interface fill:#4f86c6,color:#fff
    style Basic fill:#e8f5e9
    style Fmt fill:#e3f2fd
    style Custom fill:#fff3e0
// Custom error type dengan Unwrap untuk kompatibilitas wrapping
type ErrDB struct {
    Operasi string
    Query   string
    Err     error // underlying error
}

func (e *ErrDB) Error() string {
    return fmt.Sprintf("db error saat %s: %v", e.Operasi, e.Err)
}

// Unwrap memungkinkan errors.Is dan errors.As menembus ke underlying error
func (e *ErrDB) Unwrap() error {
    return e.Err
}

// Penggunaan
var ErrKoneksiBermasalah = errors.New("koneksi database bermasalah")

func queryDB(query string) error {
    // Simulasi error koneksi
    return &ErrDB{
        Operasi: "SELECT",
        Query:   query,
        Err:     ErrKoneksiBermasalah,
    }
}

func main() {
    err := queryDB("SELECT * FROM users WHERE id = 1")

    // errors.As untuk mengakses detail ErrDB
    var errDB *ErrDB
    if errors.As(err, &errDB) {
        fmt.Printf("Operasi: %s\n", errDB.Operasi)
        fmt.Printf("Query: %s\n", errDB.Query)
    }

    // errors.Is masih bisa menemukan ErrKoneksiBermasalah
    // karena ErrDB mengimplementasikan Unwrap
    if errors.Is(err, ErrKoneksiBermasalah) {
        fmt.Println("Koneksi database bermasalah — coba reconnect")
    }
}

Error dengan Kode untuk API #

// Error yang membawa HTTP status code dan kode error untuk API
type AppError struct {
    Code       string // kode untuk client: "USER_NOT_FOUND", "INVALID_INPUT"
    Pesan      string // pesan yang bisa ditampilkan ke user
    StatusHTTP int    // HTTP status code yang sesuai
    Err        error  // underlying error (untuk logging, tidak dikirim ke client)
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%s] %s: %v", e.Code, e.Pesan, e.Err)
    }
    return fmt.Sprintf("[%s] %s", e.Code, e.Pesan)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// Konstruktor untuk error yang umum
func ErrUserTidakDitemukan(id int) *AppError {
    return &AppError{
        Code:       "USER_NOT_FOUND",
        Pesan:      fmt.Sprintf("pengguna dengan ID %d tidak ditemukan", id),
        StatusHTTP: 404,
    }
}

func ErrInputTidakValidApp(field, pesan string) *AppError {
    return &AppError{
        Code:       "INVALID_INPUT",
        Pesan:      fmt.Sprintf("field %s: %s", field, pesan),
        StatusHTTP: 400,
    }
}

// Handler yang menggunakan AppError
func handlerGeneral(w http.ResponseWriter, r *http.Request, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(appErr.StatusHTTP)
        json.NewEncoder(w).Encode(map[string]string{
            "error": appErr.Code,
            "pesan": appErr.Pesan,
        })
        return
    }

    // Error yang tidak dikenal
    log.Printf("unexpected error: %v", err)
    http.Error(w, "internal server error", 500)
}

Pola Anti-Pattern yang Harus Dihindari #

// ✗ ANTI-PATTERN 1: Abaikan error
data, _ := os.ReadFile("config.yaml") // data bisa nil!
// Jika ReadFile gagal, data adalah nil dan penggunaan berikutnya akan panik

// ✓ BENAR: selalu periksa error
data, err := os.ReadFile("config.yaml")
if err != nil {
    return fmt.Errorf("muatConfig: %w", err)
}

// ✗ ANTI-PATTERN 2: Hanya log error tapi tetap lanjut
data, err = os.ReadFile("config.yaml")
if err != nil {
    log.Println("error:", err) // log tapi tidak return!
}
// Kode di bawah ini menggunakan data yang mungkin nil

// ✓ BENAR: log DAN return (atau tangani dengan benar)
data, err = os.ReadFile("config.yaml")
if err != nil {
    log.Printf("gagal baca config: %v", err)
    return err // atau gunakan default, atau exit
}

// ✗ ANTI-PATTERN 3: Wrap tanpa nilai tambah
func cariPengguna2(id int) error {
    err := db.Query(id)
    if err != nil {
        return fmt.Errorf("error: %w", err) // "error:" tidak menambahkan konteks apapun!
    }
    return nil
}

// ✓ BENAR: wrap dengan konteks yang bermakna
func cariPengguna3(id int) error {
    err := db.Query(id)
    if err != nil {
        return fmt.Errorf("cariPengguna id=%d: %w", id, err) // nama fungsi + argumen
    }
    return nil
}

// ✗ ANTI-PATTERN 4: Wrap error yang sudah punya konteks cukup
func cariPengguna4(id int) (*Pengguna, error) {
    p, err := cariPengguna3(id)
    if err != nil {
        // cariPengguna3 sudah menambahkan konteks yang bagus
        // wrap lagi hanya menambahkan noise
        return nil, fmt.Errorf("cariPengguna4 dipanggil dengan id %d dan gagal karena: %w", id, err)
    }
    return p, nil
}

// ✓ BENAR: wrap singkat atau langsung return jika konteks sudah cukup
func cariPengguna5(id int) (*Pengguna, error) {
    p, err := cariPengguna3(id)
    if err != nil {
        return nil, fmt.Errorf("cariPengguna5: %w", err) // ringkas, tidak redundan
    }
    return p, nil
}

// ✗ ANTI-PATTERN 5: Gunakan panic untuk error yang bisa ditangani
func bagi(a, b int) int {
    if b == 0 {
        panic("dibagi nol") // jangan panic untuk kondisi yang bisa diprediksi!
    }
    return a / b
}

// ✓ BENAR: kembalikan error untuk kondisi yang bisa terjadi
func bagiBaik(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("tidak bisa dibagi nol")
    }
    return a / b, nil
}

Pola Penggunaan di Produksi #

Error Registry — Sentinel Errors Terpusat #

// errors.go — satu file untuk semua sentinel error dalam package
package app

import "errors"

// Error autentikasi
var (
    ErrTokenKadaluarsa  = errors.New("token kadaluarsa")
    ErrTokenTidakValid  = errors.New("token tidak valid")
    ErrSesiTidakDitemukan = errors.New("sesi tidak ditemukan")
)

// Error data
var (
    ErrPenggunaTidakDitemukan = errors.New("pengguna tidak ditemukan")
    ErrProdukTidakDitemukan   = errors.New("produk tidak ditemukan")
    ErrStokKosong             = errors.New("stok kosong")
    ErrDuplikatEmail          = errors.New("email sudah terdaftar")
)

// Error sistem
var (
    ErrDatabase    = errors.New("database tidak tersedia")
    ErrCache       = errors.New("cache tidak tersedia")
    ErrLayananLuar = errors.New("layanan eksternal tidak tersedia")
)

Middleware Error Handler untuk HTTP #

// Middleware yang mengubah AppError menjadi respons JSON yang konsisten
type ErrorResponse struct {
    Kode    string `json:"kode"`
    Pesan   string `json:"pesan"`
    Detail  any    `json:"detail,omitempty"`
}

func errorHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Gunakan panic recovery untuk menangkap panic yang tidak terduga
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v\n%s", rec, debug.Stack())
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(500)
                json.NewEncoder(w).Encode(ErrorResponse{
                    Kode:  "INTERNAL_ERROR",
                    Pesan: "terjadi kesalahan internal",
                })
            }
        }()

        next(w, r)
    }
}

func kirimError(w http.ResponseWriter, err error) {
    var appErr *AppError
    if errors.As(err, &appErr) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(appErr.StatusHTTP)
        json.NewEncoder(w).Encode(ErrorResponse{
            Kode:  appErr.Code,
            Pesan: appErr.Pesan,
        })
        return
    }

    // Cek sentinel errors umum
    switch {
    case errors.Is(err, ErrPenggunaTidakDitemukan):
        w.WriteHeader(404)
        json.NewEncoder(w).Encode(ErrorResponse{
            Kode:  "NOT_FOUND",
            Pesan: "data tidak ditemukan",
        })
    case errors.Is(err, ErrTokenTidakValid),
        errors.Is(err, ErrTokenKadaluarsa):
        w.WriteHeader(401)
        json.NewEncoder(w).Encode(ErrorResponse{
            Kode:  "UNAUTHORIZED",
            Pesan: "autentikasi diperlukan",
        })
    default:
        log.Printf("unhandled error: %v", err)
        w.WriteHeader(500)
        json.NewEncoder(w).Encode(ErrorResponse{
            Kode:  "INTERNAL_ERROR",
            Pesan: "terjadi kesalahan internal",
        })
    }
}

Kapan Beralih ke Alternatif #

Tetap gunakan package errors + fmt.Errorf jika:
  ✓ Error handling standar di semua kode Go
  ✓ Mendefinisikan sentinel errors dengan errors.New
  ✓ Membungkus error dengan konteks menggunakan fmt.Errorf + %w
  ✓ Memeriksa jenis error dengan errors.Is dan errors.As
  ✓ Menggabungkan beberapa error dengan errors.Join

Pertimbangkan panic + recover jika:
  ✗ Kondisi yang benar-benar tidak mungkin terjadi (programmer error)
  ✗ Inisialisasi yang gagal saat startup dan program memang harus berhenti
  ✗ Implementasi internal yang tidak bisa mengembalikan error
     (contoh: fungsi yang dipanggil dari interface yang tidak punya error return)

Pertimbangkan library eksternal jika:
  ✗ Stack trace yang detail per error → pkg/errors atau Go 1.21+ runtime/debug
  ✗ Error dengan structured logging otomatis → zap atau slog dengan error field
  ✗ Error aggregation yang lebih canggih → hashicorp/go-multierror

Ringkasan #

  • Sentinel errors dengan errors.New adalah konstanta error yang bisa dibandingkan — definisikan di level package dengan nama ErrXxx dan gunakan errors.Is (bukan ==) untuk memeriksanya.
  • fmt.Errorf dengan %w adalah cara idiomatis untuk menambahkan konteks ke error — gunakan %w bukan %v agar errors.Is dan errors.As tetap bisa bekerja melalui rantai wrapping.
  • Konvensi penamaan error: "namaFungsi argumen: pesan" — sertakan nama fungsi dan argumen relevan agar pesan error mudah ditelusuri tanpa stack trace.
  • errors.Is menelusuri seluruh rantai wrapping untuk menemukan error yang cocok — gunakan untuk alur kontrol berdasarkan jenis error (404, 401, dll).
  • errors.As mengekstrak error bertipe tertentu dari rantai — gunakan saat butuh mengakses field atau method dari custom error type.
  • errors.Join (Go 1.20+) menggabungkan beberapa error menjadi satu — berguna untuk validasi yang mengumpulkan semua error sekaligus atau operasi paralel.
  • Custom error type dengan method Unwrap() error memungkinkan errors.Is dan errors.As menembus ke underlying error — selalu implementasikan Unwrap jika custom error membungkus error lain.
  • Jangan abaikan errordata, _ := ... hampir selalu salah. Jika memang sengaja abaikan, tambahkan komentar alasannya.
  • Jangan gunakan panic untuk kondisi error yang bisa diprediksi — panic untuk programmer error, return error untuk runtime error yang bisa terjadi.

← Sebelumnya: Strconv   Berikutnya: Encoding Json →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact