Frontend Labs
⋯

Day 7 — Error Handling

Learning Objectives

  • Build a multi-level error hierarchy for network/security code
  • Use errors.Is and errors.As for 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.


Generated from week1.md · 8 Oct 2026 16:36