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:#e8f5e9Keuntungan 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:#e8f5e9var 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:#fce4ecimport (
"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.Newadalah konstanta error yang bisa dibandingkan — definisikan di level package dengan namaErrXxxdan gunakanerrors.Is(bukan==) untuk memeriksanya.fmt.Errorfdengan%wadalah cara idiomatis untuk menambahkan konteks ke error — gunakan%wbukan%vagarerrors.Isdanerrors.Astetap 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.Ismenelusuri seluruh rantai wrapping untuk menemukan error yang cocok — gunakan untuk alur kontrol berdasarkan jenis error (404, 401, dll).errors.Asmengekstrak 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() errormemungkinkan errors.Is dan errors.As menembus ke underlying error — selalu implementasikan Unwrap jika custom error membungkus error lain.- Jangan abaikan error —
data, _ := ...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.