03 · Error Handling
No try/catch — errors are values you return and check
Node.js background?
Instead of
throw new Error() and try/catch, Go returns error as a second value. The caller always checks it — nothing is caught automatically.
The Basic Pattern
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething failed: %w", err)
}
// use result safely here
The %w verb wraps the original error so callers can inspect it. Always add context when wrapping — it builds a chain like a stack trace.
Real Code — lp-coupon-api
// internal/services/coupon_service.go
func (s *CouponService) GetCouponByID(ctx context.Context, id string) (*models.Coupon, error) {
coupon, err := s.couponRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("GetCouponByID: %w", err)
}
return coupon, nil
}
Notice: the handler calls the service, the service calls the repo, each layer wraps the error with context. By the time the error reaches the handler, it has a full breadcrumb trail.
errors.Is and errors.As
Use these to inspect wrapped errors rather than string matching.
var ErrNotFound = errors.New("not found")
// Somewhere deep in the call chain, this error was wrapped:
// fmt.Errorf("GetCouponByID: %w", ErrNotFound)
// Check if the chain contains ErrNotFound:
if errors.Is(err, ErrNotFound) {
// return HTTP 404
}
// Extract a typed error:
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Println(validationErr.Field)
}
Creating Custom Errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
Any type with an Error() string method satisfies the error interface.
panic — When to Use It
Do not use panic for business logic errors. panic is for programmer mistakes (impossible state, nil where nil should never occur). Always return an error instead.
// BAD — don't do this
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // kills the server!
}
return user
}
// GOOD
func GetUser(id string) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("GetUser: %w", err)
}
return user, nil
}
Fiber's recover middleware catches panics at the HTTP boundary, but you should never rely on it for expected error cases.
Key Takeaways
- Functions return
(T, error)— checkerr != nilimmediately after every call - Wrap errors with
fmt.Errorf("context: %w", err)— adds breadcrumb, preserves chain - Use
errors.Isto check for a specific sentinel error anywhere in the chain - Use
errors.Asto extract a typed error from the chain panicis for programmer bugs, not expected failures — always return an error instead