Error Handling di Go: Love It or Hate It
Go's explicit error handling memang kontroversial. Banyak developer mengeluh tentang if err != nil yang berulang. Tapi setelah beberapa tahun menggunakan Go, saya malah menghargai approach ini. Explicit error handling membuat code lebih predictable dan lebih mudah di-debug.
Dasar Error Interface
type error interface {
Error() string
}
func main() {
data, err := fetchData()
if err != nil {
log.Printf("Error fetching data: %v", err)
return err
}
}
Error Wrapping: Context is King
Salah satu best practices yang saya terapkan selalu adalah error wrapping. Error tanpa context sulit di-debug di production.
// Bad: error tanpa context
func GetUser(id string) (*User, error) {
var user User
err := db.First(&user, id).Error
return &user, err
}
// Good: error dengan context
func GetUser(id string) (*User, error) {
var user User
if err := db.First(&user, id).Error; err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return &user, nil
}
// Unwrap untuk cek error type
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
Sentinel Errors: Konsisten dan Testable
var (
ErrUserNotFound = errors.New("user not found")
ErrUnauthorized = errors.New("unauthorized")
ErrDuplicateEmail = errors.New("email already exists")
ErrInsufficientPlan = errors.New("plan upgrade required")
)
func handleGetUser(c *fiber.Ctx) error {
user, err := svc.GetUser(id)
if err != nil {
switch {
case errors.Is(err, ErrUserNotFound):
return c.Status(404).JSON(fiber.Map{"error": err.Error()})
case errors.Is(err, ErrUnauthorized):
return c.Status(403).JSON(fiber.Map{"error": err.Error()})
default:
return c.Status(500).JSON(fiber.Map{"error": "internal server error"})
}
}
return c.JSON(user)
}
Error Handling Anti-Patterns
- Panic recovery: Gunakan recover() untuk panic, tapi jangan pakai panic sebagai error flow control
- Ignoring errors: result, _ := doSomething() ini time bomb
- Returning nil error dengan zero value: Jangan pernah return (nil, nil) untuk error case
Yang saya pelajari: error handling di Go memang verbose, tapi ini menjadikan codebase lebih maintainable dalam jangka panjang. Dan dengan Go 1.13+ error wrapping, tracing error menjadi jauh lebih mudah.
Custom Error Types dengan Stack Trace
Untuk debugging di production, stack trace sangat membantu. Saya biasanya buat custom error type yang menyimpan stack trace:
import "runtime/debug"
type AppError struct {
Code string
Message string
StackTrace string
Cause error
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
func NewAppError(code, message string, cause error) *AppError {
return &AppError{
Code: code,
Message: message,
StackTrace: string(debug.Stack()),
Cause: cause,
}
}
Dengan approach ini, setiap error yang terjadi di production bisa langsung di-trace ke baris kode yang menyebabkannya. Sangat membantu saat debugging issue di production.
Graceful Degradation Pattern
Tidak semua error harus menghentikan seluruh request. Kadang lebih baik degrade gracefully. Misalnya, jika cache service down, fallback ke database langsung:
func GetUserProfile(userID string) (*Profile, error) {
// Try cache first
profile, err := cache.Get("profile:" + userID)
if err == nil {
return profile, nil
}
// Fallback to database
profile, err = db.GetProfile(userID)
if err != nil {
return nil, fmt.Errorf("get profile %s: %w", userID, err)
}
// Update cache asynchronously
go cache.Set("profile:"+userID, profile, time.Minute*5)
return profile, nil
}
Pattern ini membuat aplikasi lebih resilient terhadap partial failures.