Day 7 — Error Handling
Learning Objectives
- Build a multi-level error hierarchy for network/security code
- Use
errors.Isanderrors.Asfor programmatic error handling - Understand timing-safe comparisons (relevant to auth in Week 11)
Key Concepts
// Sentinel errors — compare with errors.Is
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
// Wrapped errors — preserve context while keeping the chain
return fmt.Errorf("storage.Get %q: %w", key, ErrNotFound)
// Check the chain
if errors.Is(err, ErrNotFound) { /* handle */ }
// Structured error types — use when you need data in the error
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
// Extract the structured type
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("invalid field:", ve.Field)
}
Exercise: Security Error System
package main
import (
"errors"
"fmt"
)
// Sentinel errors for the security stack
var (
ErrNotFound = errors.New("not found")
ErrStorageFull = errors.New("storage full")
ErrInvalidKey = errors.New("invalid key")
ErrUnauthorized = errors.New("unauthorized")
ErrRateLimited = errors.New("rate limited")
ErrBlocked = errors.New("blocked")
)
// RequestError carries request context for audit logging
type RequestError struct {
Code int // HTTP status to return
Public string // safe to show to client
Internal string // log this, never expose
Cause error
}
func (e *RequestError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Internal, e.Cause)
}
func (e *RequestError) Unwrap() error { return e.Cause }
func newAuthError(internal string, cause error) *RequestError {
return &RequestError{
Code: 401,
Public: "authentication required", // never leak the real reason
Internal: internal,
Cause: cause,
}
}
func newBlockedError(reason string) *RequestError {
return &RequestError{
Code: 403,
Public: "forbidden",
Internal: reason,
Cause: ErrBlocked,
}
}
func authenticate(apiKey string) error {
if apiKey == "" {
return newAuthError("missing api key header", ErrUnauthorized)
}
if apiKey == "blocked-key" {
return newBlockedError("api key on blocklist")
}
if apiKey != "valid-key" {
return newAuthError(fmt.Sprintf("api key not found: %q", apiKey), ErrNotFound)
}
return nil
}
func handleRequest(apiKey string) {
err := authenticate(apiKey)
if err == nil {
fmt.Println("request allowed")
return
}
// Log the internal detail
fmt.Printf("AUDIT: %v\n", err)
// Return the public message to client
var reqErr *RequestError
if errors.As(err, &reqErr) {
fmt.Printf("CLIENT RESPONSE: %d %s\n", reqErr.Code, reqErr.Public)
}
// Programmatic checks
if errors.Is(err, ErrBlocked) {
fmt.Println("ACTION: add to block list")
}
}
func main() {
handleRequest("valid-key")
fmt.Println("---")
handleRequest("")
fmt.Println("---")
handleRequest("blocked-key")
fmt.Println("---")
handleRequest("unknown-key")
}
Real-World Context
Never return internal error details to API callers — that's information disclosure (OWASP A01). The RequestError pattern separates what you log from what you expose. errors.Is lets middleware handle auth errors differently from storage errors without string matching.