Fail Fast #

There are two ways a system reacts to invalid conditions. The first: keep going, hide the error, try to handle it as best you can, and hope nothing breaks. The second: stop immediately, report clearly what’s wrong, and don’t let invalid state spread any further. Fail Fast is the second way — and it’s not about making a system that crashes easily, but about making a system that’s honest about its own condition. A system that fails quickly with a clear message is far easier to debug and fix than a system that fails slowly with a mysterious error far from the source of the problem. The Fail Fast principle states: detect error conditions as early as possible, report them with enough context, and don’t continue execution with invalid state. This article covers four levels of applying Fail Fast — from input validation to system architecture — its difference from Fail Safe, when panic vs error is appropriate in Go, and when Fail Fast is actually dangerous and needs to be combined with recovery strategies.

Why Fail Fast? #

Imagine a function that receives an empty database configuration, then continues execution and tries to open a connection — which of course fails. The error that appears is "connection refused to :0" instead of "database DSN is required". The engineer debugging has to work backward from the connection error to the code that should have validated the config — wasting unnecessary time.

Without Fail Fast:                   With Fail Fast:
──────────────────────────────      ──────────────────────────────────
Empty config read                    Empty config read
  ↓                                    ↓
Passed to the DB connector           Validation: DSN empty → STOP
  ↓                                    ↓
Try to connect to ":0"               Error immediately:
  ↓                                    "database DSN is required"
"connection refused to :0"
  ↓                                  The engineer immediately knows
  ↓                                  what's wrong and where to fix it
Next request: panic
in the middle of a handler
  ↓
"nil pointer dereference"
  ↓
Engineer must trace back from
nil pointer → find where db
is initialized → find the config
→ only then know the problem

The distance between the error source and the place the error is detected is unnecessary debugging cost. Fail Fast minimizes that distance.

flowchart LR
    subgraph SLOW["Without Fail Fast — error far from its source"]
        direction TB
        S1["Empty config"] --> S2["Passed through"] --> S3["Connection fails"] --> S4["Nil pointer"] --> S5["💥 Panic in handler\n(far from the source)"]
    end

    subgraph FAST["With Fail Fast — error near its source"]
        direction TB
        F1["Empty config"] --> F2["💥 Validation fails\n(near the source)"]
    end

    style S5 fill:#D9534F,color:#fff
    style F2 fill:#5CB85C,color:#fff

Level 1 — Fail Fast in Input Validation #

The first and most basic level: validate all input at the entry point — not in the middle of processing.

// ANTI-PATTERN: validation scattered and late
func TransferFunds(fromID, toID string, amount float64) error {
    // No validation at the start
    from, err := repo.FindAccount(fromID)
    if err != nil {
        return err // error too late — already queried the DB for a possibly empty ID
    }

    to, err := repo.FindAccount(toID)
    if err != nil {
        return err
    }

    if from.Balance < amount { // business validation only happens here
        return errors.New("insufficient balance")
    }

    if amount <= 0 { // validation should be first, but it's last
        return errors.New("amount must be positive")
    }

    return repo.Transfer(from, to, amount)
}

// CORRECT: all validation up front — fail fast before doing any operation
func TransferFunds(ctx context.Context, fromID, toID string, amount float64) error {
    // === Fail Fast: validate all inputs before doing anything ===
    if fromID == "" {
        return errors.New("fromID is required")
    }
    if toID == "" {
        return errors.New("toID is required")
    }
    if fromID == toID {
        return errors.New("cannot transfer to the same account")
    }
    if amount <= 0 {
        return fmt.Errorf("amount must be positive, got %.2f", amount)
    }
    if amount > MaxSingleTransferAmount {
        return fmt.Errorf("amount %.2f exceeds maximum single transfer limit %.2f",
            amount, MaxSingleTransferAmount)
    }

    // Only after all validations pass, start the operations
    from, err := repo.FindAccount(ctx, fromID)
    if err != nil {
        return fmt.Errorf("transferFunds: find source account: %w", err)
    }

    if from.Balance < amount {
        return fmt.Errorf("insufficient balance: have %.2f, need %.2f",
            from.Balance, amount)
    }

    to, err := repo.FindAccount(ctx, toID)
    if err != nil {
        return fmt.Errorf("transferFunds: find destination account: %w", err)
    }

    return repo.Transfer(ctx, from, to, amount)
}
Fail Fast in input validation means all guard clauses at the top, before any operation starts. This isn’t just KISS (guard clauses replacing nested conditions) — it’s also Fail Fast because errors are reported before any side effects.


Level 2 — Fail Fast at Startup #

Startup is the best opportunity to Fail Fast. It’s better for an application to not start with a clear error message than to start with wrong configuration and crash mysteriously in production the first time a request touches that feature.

// ANTI-PATTERN: the app starts even with incomplete config
func main() {
    cfg := &Config{
        DBDSN:  os.Getenv("DATABASE_URL"), // might be empty
        Port:   os.Getenv("PORT"),         // might be empty
        APIKey: os.Getenv("PAYMENT_API_KEY"), // might be empty
    }
    // No validation — the app starts, crashes later
    startServer(cfg)
}

// CORRECT: validate everything required before the app is considered ready
func main() {
    cfg, err := loadAndValidateConfig()
    if err != nil {
        // Use log.Fatal not panic — cleaner message in the logs
        log.Fatalf("startup failed: invalid configuration:\n%v", err)
    }

    // Check connectivity to all critical dependencies before serving
    if err := checkDependencies(cfg); err != nil {
        log.Fatalf("startup failed: dependency check:\n%v", err)
    }

    log.Printf("all checks passed, starting server on :%d", cfg.Port)
    startServer(cfg)
}

func loadAndValidateConfig() (*Config, error) {
    cfg := &Config{
        DBDSN:       os.Getenv("DATABASE_URL"),
        Port:        getEnvInt("PORT", 8080),
        APIKey:      os.Getenv("PAYMENT_API_KEY"),
        JWTSecret:   os.Getenv("JWT_SECRET"),
        KafkaBroker: os.Getenv("KAFKA_BROKER"),
    }

    var errs []string

    if cfg.DBDSN == "" {
        errs = append(errs, "DATABASE_URL is required")
    }
    if cfg.Port <= 0 || cfg.Port > 65535 {
        errs = append(errs, fmt.Sprintf("PORT must be between 1-65535, got %d", cfg.Port))
    }
    if cfg.APIKey == "" {
        errs = append(errs, "PAYMENT_API_KEY is required")
    }
    if len(cfg.JWTSecret) < 32 {
        errs = append(errs, "JWT_SECRET must be at least 32 characters")
    }
    if cfg.KafkaBroker == "" {
        errs = append(errs, "KAFKA_BROKER is required")
    }

    if len(errs) > 0 {
        return nil, fmt.Errorf("missing or invalid configuration:\n  - %s",
            strings.Join(errs, "\n  - "))
    }
    return cfg, nil
}

func checkDependencies(cfg *Config) error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Check the database
    db, err := sql.Open("postgres", cfg.DBDSN)
    if err != nil {
        return fmt.Errorf("database: open connection: %w", err)
    }
    defer db.Close()
    if err := db.PingContext(ctx); err != nil {
        return fmt.Errorf("database: ping failed (is the database running?): %w", err)
    }
    log.Println("✓ database connection ok")

    // Check schema migrations are up to date
    if err := checkMigrationVersion(ctx, db); err != nil {
        return fmt.Errorf("database: migration check: %w", err)
    }
    log.Println("✓ database schema up to date")

    return nil
}
Comprehensive startup validation gives a double benefit: preventing mysterious runtime crashes, and explicitly documenting all the dependencies the application requires in one place.


Level 3 — Fail Fast with panic vs error in Go #

Go distinguishes two mechanisms for reporting unexpected conditions: error for conditions expected to occur and handled by the caller, and panic for conditions that should never happen — programmer errors or violated invariants.

Fail Fast with an appropriate panic is a valid tool in Go, but its use must be limited and well documented.

// WHEN TO USE error (conditions that can happen and callers must handle):
func FindUser(ctx context.Context, id string) (*User, error) {
    if id == "" {
        return nil, errors.New("id is required") // the caller must handle this
    }
    user, err := db.QueryUser(ctx, id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrUserNotFound // a valid, expected condition
    }
    if err != nil {
        return nil, fmt.Errorf("findUser: %w", err)
    }
    return user, nil
}

// WHEN TO USE panic (programmer errors — should never happen):

// 1. An injected dependency is nil — this is a bug, not a runtime condition
func NewOrderService(repo OrderRepository, notifier Notifier) *OrderService {
    if repo == nil {
        panic("NewOrderService: repo must not be nil") // programmer error
    }
    if notifier == nil {
        panic("NewOrderService: notifier must not be nil") // programmer error
    }
    return &OrderService{repo: repo, notifier: notifier}
}

// 2. A switch/select that should be exhaustive
func statusToHTTP(s order.Status) int {
    switch s {
    case order.StatusPending:
        return http.StatusAccepted
    case order.StatusConfirmed:
        return http.StatusOK
    case order.StatusCancelled:
        return http.StatusGone
    default:
        // If a new Status is added but this switch isn't updated,
        // panicking is better than silently returning a wrong value
        panic(fmt.Sprintf("statusToHTTP: unhandled status %q", s))
    }
}

// 3. Package-level initialization that must succeed (regex, templates)
var (
    // MustCompile is the standard Go idiom for Fail Fast at init
    emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
    // If the regex is invalid, there's no point in the app running
)

// Writing your own "Must" functions for similar cases:
func mustParseTemplate(name, tmpl string) *template.Template {
    t, err := template.New(name).Parse(tmpl)
    if err != nil {
        panic(fmt.Sprintf("mustParseTemplate %q: %v", name, err))
    }
    return t
}

var welcomeTemplate = mustParseTemplate("welcome", `
    <h1>Welcome, {{.Name}}!</h1>
    <p>Your account has been created.</p>
`)
Guidance on when panic vs error is appropriate:

USE error WHEN:
  ✓ The condition can happen at runtime due to input or external state
  ✓ The caller can and must make decisions based on this error
  ✓ There's a reasonable way to handle this condition
  Examples: file not found, network timeout, record not found, validation failed

USE panic WHEN:
  ✓ This is a programmer error — it should never happen
  ✓ Returning an error would hide a serious bug
  ✓ This condition signals a violated system invariant
  Examples: nil dependency injected, non-exhaustive switch cases,
            initialization that should always succeed

DON'T panic WHEN:
  ✗ The condition can happen due to user or external system input
  ✗ It's a library or package that other code may use
    (a library's panic can't be prevented by the caller)
  ✗ Only because "this shouldn't happen" without full certainty

Level 4 — Fail Fast at the System Architecture Level #

At the system level, Fail Fast means detecting and responding to unhealthy conditions as quickly as possible — before errors spread to other components or to users.

Fail Fast in the API layer with informative errors:

// ANTI-PATTERN: swallowing errors and returning generic responses
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "bad request", 400) // not informative
        return
    }

    result, err := h.service.CreateOrder(r.Context(), req)
    if err != nil {
        http.Error(w, "error", 500) // ← no information for debugging
        return
    }

    json.NewEncoder(w).Encode(result)
}

// CORRECT: errors informative enough for debugging without exposing internal details
type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    TraceID string `json:"trace_id,omitempty"` // for log correlation
}

func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    traceID := middleware.TraceIDFromContext(r.Context())

    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeAPIError(w, http.StatusBadRequest, APIError{
            Code:    "INVALID_REQUEST_BODY",
            Message: "request body is not valid JSON",
            TraceID: traceID,
        })
        return
    }

    result, err := h.service.CreateOrder(r.Context(), req)
    if err != nil {
        // Log internal details for debugging
        slog.Error("create order failed",
            "trace_id", traceID,
            "user_id", req.UserID,
            "error", err,
        )

        // Translate into the right error for the client
        switch {
        case errors.Is(err, order.ErrUserNotActive):
            writeAPIError(w, http.StatusForbidden, APIError{
                Code:    "USER_NOT_ACTIVE",
                Message: "your account is not active",
                TraceID: traceID,
            })
        case errors.Is(err, order.ErrInsufficientStock):
            writeAPIError(w, http.StatusConflict, APIError{
                Code:    "INSUFFICIENT_STOCK",
                Message: "one or more items are out of stock",
                TraceID: traceID,
            })
        default:
            writeAPIError(w, http.StatusInternalServerError, APIError{
                Code:    "INTERNAL_ERROR",
                Message: "an unexpected error occurred",
                TraceID: traceID, // the trace ID so support can correlate with logs
            })
        }
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(result)
}
Fail Fast in the domain model with invariant enforcement:

// A domain model that enforces its own invariants
// instead of relying on callers to always remember to validate

type Money struct {
    amount   int64  // in cents, cannot be negative
    currency string // must be ISO 4217
}

// A constructor that Fails Fast — there's no way to create invalid Money
func NewMoney(amount int64, currency string) (Money, error) {
    if amount < 0 {
        return Money{}, fmt.Errorf("money amount cannot be negative: %d", amount)
    }
    if !isValidCurrency(currency) {
        return Money{}, fmt.Errorf("invalid currency code: %q", currency)
    }
    return Money{amount: amount, currency: currency}, nil
}

// Or a Must version for contexts guaranteed to be valid
func MustNewMoney(amount int64, currency string) Money {
    m, err := NewMoney(amount, currency)
    if err != nil {
        panic(fmt.Sprintf("MustNewMoney: %v", err))
    }
    return m
}

func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, fmt.Errorf("cannot add %s and %s", m.currency, other.currency)
    }
    return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}

// This struct can't be initialized with invalid values
// because its fields are private and only creatable via NewMoney
#

Fail Fast vs Fail Safe #

Fail Fast is often contrasted with Fail Safe — and the two aren’t mutually exclusive choices. What’s right depends on the context and the consequences of failure.

FAIL FAST:
  Principle: stop immediately when an invalid condition is detected
  Goal     : prevent invalid state from spreading; easier debugging
  Good for : startup validation, developer-facing errors, programmer errors,
             conditions that should never happen at all
  Examples : incomplete config → refuse to start
             nil dependency → panic
             non-exhaustive switch case → panic

FAIL SAFE:
  Principle: when failing, fall into a safe or degraded mode
  Goal     : maintain availability even when a component isn't working
  Good for : user-facing features, degraded modes that still provide value,
             conditions that reasonably happen in production
  Examples : recommendation service down → show popular products as a default
             cache miss → fall back to the database
             payment gateway A down → try payment gateway B

COMBINING BOTH (most common):
  Fail Fast for conditions that must not happen (startup, programmer errors)
  Fail Safe for conditions that can happen at runtime (dependency down, timeouts)
flowchart TD
    ERR["Error Condition\nDetected"]

    Q1{"Should this\nnever happen?\n(programmer error /\ninvariant violated)"}
    Q2{"Is there a fallback that\nprovides real value\nfor the user?"}
    Q3{"Is this at\nstartup / initialization?"}

    FF1["Fail Fast\npanic with a clear message"]
    FF2["Fail Fast\nlog.Fatal at startup"]
    FS["Fail Safe\nfallback / degraded mode"]
    ERR2["Return error\nto the caller with context"]

    ERR --> Q1
    Q1 -->|"Yes"| Q3
    Q1 -->|"No"| Q2
    Q3 -->|"Yes"| FF2
    Q3 -->|"No"| FF1
    Q2 -->|"Yes"| FS
    Q2 -->|"No"| ERR2

    style FF1 fill:#D9534F,color:#fff
    style FF2 fill:#D9534F,color:#fff
    style FS fill:#4C9BE8,color:#fff
    style ERR2 fill:#5CB85C,color:#fff

Fail Fast in Dart/Flutter #

In Dart, Fail Fast most often appears in two contexts: assertions for developer invariants, and early returns in business logic.

// ANTI-PATTERN: a constructor silently accepting invalid state
type Order struct {
    id     string
    userId string
    items  []OrderItem
    total  float64
}

// No validation — can be created with a negative total or empty items
func NewOrder(id, userId string, items []OrderItem, total float64) *Order {
    return &Order{id: id, userId: userId, items: items, total: total}
}

// CORRECT: Fail Fast in the constructor with explicit validation
// Go has no assert builtin — the idiomatic equivalent is explicit
// checks that return an error (or panic for programmer invariants)
func NewOrder(id, userId string, items []OrderItem) (*Order, error) {
    if id == "" {
        return nil, errors.New("id must not be empty")
    }
    if userId == "" {
        return nil, errors.New("userId must not be empty")
    }
    if len(items) == 0 {
        return nil, errors.New("items must not be empty")
    }

    total := 0.0
    for _, item := range items {
        total += item.UnitPrice * float64(item.Quantity)
    }

    return &Order{id: id, userId: userId, items: items, total: total}, nil
}

// Fail Fast in a service with early returns
type OrderService struct {
    userRepo  UserRepository
    orderRepo OrderRepository
}

func (s *OrderService) CreateOrder(ctx context.Context, userID string, cartItems []CartItem) (string, error) {
    // Fail Fast: validate everything before any operation
    if userID == "" {
        return "", errors.New("userID must not be empty")
    }
    if len(cartItems) == 0 {
        return "", errors.New("cannot create order with empty cart")
    }

    // Check the user is active before starting
    user, err := s.userRepo.FindByID(ctx, userID)
    if err != nil {
        return "", fmt.Errorf("order service: find user: %w", err)
    }
    if !user.IsActive {
        return "", errors.New("user account is not active")
    }

    // Only after all validations pass, process
    order, err := NewOrder(generateID(), userID, toOrderItems(cartItems))
    if err != nil {
        return "", err
    }
    return s.orderRepo.Save(ctx, order)
}
#

Fail Fast and Testability #

One immediate benefit of Fail Fast that’s often not recognized is better testability. Code that Fails Fast is easier to test because:

// With Fail Fast, tests can verify that invalid conditions
// are clearly and quickly rejected — not producing mysterious side effects

func TestTransferFunds_FailFast(t *testing.T) {
    tests := []struct {
        name    string
        fromID  string
        toID    string
        amount  float64
        wantErr string
    }{
        {
            name:    "empty fromID",
            fromID:  "",
            toID:    "acc-456",
            amount:  100,
            wantErr: "fromID is required",
        },
        {
            name:    "same account",
            fromID:  "acc-123",
            toID:    "acc-123",
            amount:  100,
            wantErr: "cannot transfer to the same account",
        },
        {
            name:    "negative amount",
            fromID:  "acc-123",
            toID:    "acc-456",
            amount:  -50,
            wantErr: "amount must be positive",
        },
        {
            name:    "zero amount",
            fromID:  "acc-123",
            toID:    "acc-456",
            amount:  0,
            wantErr: "amount must be positive",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // This test needs no database mock at all
            // because Fail Fast happens before the database is touched
            err := TransferFunds(context.Background(), tt.fromID, tt.toID, tt.amount)

            if err == nil {
                t.Fatal("expected error but got nil")
            }
            if !strings.Contains(err.Error(), tt.wantErr) {
                t.Errorf("error = %q, want to contain %q", err.Error(), tt.wantErr)
            }
        })
    }
}
// This test is a pure unit test — no database, no complex mocks
// Because validation happens before any interaction with dependencies
#

When Fail Fast Is Dangerous #

Fail Fast applied without considering context can produce a system that’s too fragile — crashing for things that could actually be handled with graceful degradation.

APPROPRIATE FAIL FAST:
  ✓ Startup — better to not start than to start with a wrong state
  ✓ Dependency injection — a nil dependency is a programmer error
  ✓ Domain invariants — invalid data must not enter the domain
  ✓ Exhaustive switches — an unhandled case is a programmer error
  ✓ Package-level initialization — regex, templates, schemas must be valid

DANGEROUS FAIL FAST (consider Fail Safe or graceful handling):
  ✗ Bad user requests — reject with a clear error, don't crash the server
  ✗ One item failing from a batch — fail that item, continue the rest
  ✗ Non-critical features failing — show a fallback, don't take everything down
  ✗ Unavailable external dependencies — circuit breaker + fallback
  ✗ Old data failing new validation during migrations
    — update gradually, don't reject everything

THE RIGHT QUESTION before deciding Fail Fast or not:
  "If this condition happens, can the whole system keep functioning
   correctly if we continue?"
  → Yes: Fail Fast
  → No, but there's a fallback that still provides value: Fail Safe
  → No, the caller can handle it: return an error
Fail Fast in libraries consumed by others needs extra consideration. A panic from a library can’t be prevented by the caller except through explicit recover. If you’re writing a public library, consider always returning error instead of panic — except for conditions that are truly unrecoverable (like regexp.MustCompile with hardcoded regex that can never be invalid).

Anti-Patterns at a Glance #

// ✗ Late validation — already queried the DB before checking input
func CreateUser(email, password string) error {
    hashedPw := hashPassword(password) // the operation runs first
    if email == "" { return errors.New("email required") } // validation too late
    return db.Save(email, hashedPw)
}

// ✗ Swallowing errors without a meaningful fallback
func GetUserName(id string) string {
    user, err := db.FindUser(id)
    if err != nil {
        return "" // error ignored, the caller doesn't know there's a problem
    }
    return user.Name
}

// ✗ The app starts even when dependencies are unavailable
func main() {
    cfg := loadConfig() // not validated
    db := connectDB(cfg.DSN) // may be nil if the DSN is empty
    startServer(db) // crashes later on the first request
}

// ✗ panic for conditions that can occur normally
func FindOrder(id string) *Order {
    order, err := db.Find(id)
    if err != nil {
        panic(err) // a valid condition (not found) turned into a panic
    }
    return order
}

// ✗ Health checks that are always healthy
func health(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("ok")) // doesn't check dependencies — a hidden SPoF
}

// ✗ Errors without enough context
func processPayment(id string) error {
    _, err := db.FindPayment(id)
    if err != nil {
        return err // no context: which operation? which ID?
    }
    return nil
}
// ✓ Should be:
// return fmt.Errorf("processPayment %s: find payment: %w", id, err)
#

Fail Fast Review Checklist #

INPUT VALIDATION:
  □ All input validation at the start of the function — before any operation
  □ Error messages clear, naming which field is wrong and why
  □ No operation runs with invalid input

STARTUP AND INITIALIZATION:
  □ All required environment variables validated at startup
  □ Connections to critical dependencies checked before serving traffic
  □ Package-level initialization uses Must patterns for values
    that should always be valid

PANIC VS ERROR:
  □ panic only for programmer errors — violated invariants, nil dependencies
  □ error for runtime conditions callers can and must handle
  □ Libraries don't use panic for conditions that can occur normally

ARCHITECTURE:
  □ API error responses contain a code, message, and enough trace ID
    for debugging without exposing internal details
  □ Domain models can't be instantiated in invalid states
  □ Health check endpoints reflect the real condition of dependencies

COMBINED WITH FAIL SAFE:
  □ External dependencies have circuit breakers or fallbacks — not crashes
  □ Non-critical features have degraded modes — not total outages
  □ Bad user requests get clear errors — not server crashes

Summary #

  • Fail Fast means detecting and reporting invalid conditions as early as possible — before invalid state spreads further. The distance between the error source and where the error is detected is unnecessary debugging cost.
  • Level 1 — Input validation: all guard clauses at the start of the function, before any operation. Specific errors naming which field is wrong and why.
  • Level 2 — Startup: validate all config and dependency connections before serving. Better to not start with a clear message than to crash mysteriously at runtime.
  • Level 3 — panic vs error in Go: error for runtime conditions callers can handle; panic for programmer errors and violated invariants. MustX patterns for initialization that should always succeed.
  • Level 4 — Architecture: informative API errors with trace IDs; domain models that can’t be instantiated in invalid states; health checks reflecting real dependency conditions.
  • Fail Fast vs Fail Safe: not mutually exclusive. Fail Fast for startup and programmer errors; Fail Safe for external dependencies and non-critical features with meaningful fallbacks.
  • Fail Fast improves testability: validation happening before dependencies are touched can be tested as pure unit tests without mocks.
  • When it’s dangerous: don’t Fail Fast for bad user requests (return an error), for one failed item in a batch (skip and continue), or for external dependencies (circuit breaker + fallback).
  • In public libraries: avoid panic for conditions that can occur normally — callers can’t prevent a library’s panic except through explicit recover.

← Previous: SPoF   Next: SPoF →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact