Skip to content

Fail Fast

Detect and report problems as early as possible, before they cascade into larger failures.

Key Insight

Fail before you start, not in the middle. Validate preconditions before executing expensive or irreversible operations.


Overview

Fail fast is a design pattern that validates preconditions before executing expensive or irreversible operations. When validation fails, the system immediately reports the error rather than proceeding and failing later in an unpredictable state.

flowchart TD
    subgraph validation[Validation Phase]
        A[Request] --> B{Preconditions Met?}
    end

    subgraph execution[Execution Phase]
        C[Execute Operation]
        D[Success]
    end

    subgraph failure[Failure Phase]
        E[Report Error]
        F[No Side Effects]
    end

    B -->|Yes| C
    C --> D
    B -->|No| E
    E --> F

    %% Ghostty Hardcore Theme
    style A fill:#65d9ef,color:#1b1d1e
    style B fill:#fd971e,color:#1b1d1e
    style C fill:#a7e22e,color:#1b1d1e
    style D fill:#a7e22e,color:#1b1d1e
    style E fill:#f92572,color:#1b1d1e
    style F fill:#f92572,color:#1b1d1e

The key insight: fail before you start, not in the middle.


When to Apply

Scenario Apply Fail Fast? Reasoning
Invalid user input Yes User error, report immediately
Missing required config Yes Can't continue safely
Insufficient permissions Yes Operation will fail anyway
Resource allocation failure Yes Partial allocation is worse
Network timeout No Use graceful degradation
Cache miss No Expensive path still works

Decision rule: Fail fast on precondition failures. Degrade gracefully on runtime failures.


Real-World Examples

GitHub Actions Workflow Validation

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      # Fail fast: Check all preconditions before expensive operations
      - name: Validate environment
        run: |
          # Required secrets
          [ -n "${{ secrets.DEPLOY_TOKEN }}" ] || { echo "::error::DEPLOY_TOKEN not set"; exit 1; }

          # Required tools
          command -v kubectl >/dev/null || { echo "::error::kubectl not found"; exit 1; }

          # Required access
          kubectl auth can-i create deployments || { echo "::error::No deploy permission"; exit 1; }

      # Now safe to proceed with expensive operations
      - name: Deploy
        run: kubectl apply -f manifests/

Go Function with Precondition Validation

func ProcessOrder(order *Order) error {
    // Fail fast: validate all preconditions upfront
    if order == nil {
        return errors.New("order is nil")
    }
    if order.CustomerID == "" {
        return errors.New("customer ID required")
    }
    if len(order.Items) == 0 {
        return errors.New("order has no items")
    }
    if order.Total <= 0 {
        return errors.New("invalid order total")
    }

    // All preconditions met, safe to proceed
    return processValidOrder(order)
}

Fail Fast vs Graceful Degradation

These patterns are complementary, not contradictory:

flowchart TD
    A[Error Occurs] --> B{Error Type?}

    B -->|Precondition Failure| C[Fail Fast]
    B -->|Runtime Failure| D[Graceful Degradation]

    C --> E[Report Error Immediately]
    D --> F[Try Fallback]

    %% Ghostty Hardcore Theme
    style A fill:#65d9ef,color:#1b1d1e
    style B fill:#fd971e,color:#1b1d1e
    style C fill:#f92572,color:#1b1d1e
    style D fill:#a7e22e,color:#1b1d1e
    style E fill:#f92572,color:#1b1d1e
    style F fill:#a7e22e,color:#1b1d1e
Error Type Pattern Example
Missing config Fail Fast Can't start without database URL
Database timeout Graceful Degradation Retry, then use cache
Invalid input Fail Fast Reject malformed request
API unavailable Graceful Degradation Use backup endpoint
Insufficient permissions Fail Fast Don't attempt forbidden operation
Rate limited Graceful Degradation Exponential backoff

Fail Fast Techniques

Comprehensive techniques for implementing fail fast patterns:

Early Termination

Stop execution immediately when errors occur:

  • Shell strict mode (set -euo pipefail)
  • GitHub Actions matrix fail-fast
  • Go error propagation
  • Circuit breakers

Strict Mode Execution

Enable strictest validation and error detection:

  • Shell/TypeScript/Go strict modes
  • Linter enforcement
  • Schema validation

Assertion Patterns

Validate assumptions and fail if they're wrong:

  • Runtime assertions
  • Contract validation (pre/post conditions)
  • Invariant checks
  • Type guards

Error Escalation

Determine when to throw vs return, panic vs recover:

  • Throw vs return error
  • Error aggregation vs first-error-wins
  • Panic vs recoverable errors
  • Exit codes

Timeout Enforcement

Prevent operations from running indefinitely:

  • Operation timeouts
  • Job timeouts
  • Circuit breaker timeouts
  • Deadlock detection

Anti-Patterns

1. Late Validation

Validating after side effects have occurred.

// Bad: creates file before validating
func ProcessFile(path string, data []byte) error {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer f.Close()

    if len(data) == 0 {
        return errors.New("empty data")  // File already created!
    }
    return f.Write(data)
}

// Good: validate before side effects
func ProcessFile(path string, data []byte) error {
    if len(data) == 0 {
        return errors.New("empty data")
    }

    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer f.Close()

    return f.Write(data)
}

2. Swallowing Errors

Continuing despite failures.

# Bad: ignores validation failure
- name: Validate and deploy
  run: |
    ./validate.sh || true  # Swallowed!
    ./deploy.sh

# Good: fail on validation error
- name: Validate
  run: ./validate.sh

- name: Deploy
  run: ./deploy.sh

3. Partial Execution

Executing some operations before checking all preconditions.

// Bad: partial execution on failure
func TransferFunds(from, to string, amount int) error {
    if err := debit(from, amount); err != nil {
        return err
    }
    // What if 'to' account doesn't exist?
    if err := credit(to, amount); err != nil {
        return err  // Money debited but not credited!
    }
    return nil
}

// Good: validate everything first
func TransferFunds(from, to string, amount int) error {
    // Validate all preconditions
    if !accountExists(from) {
        return errors.New("source account not found")
    }
    if !accountExists(to) {
        return errors.New("destination account not found")
    }
    if balance(from) < amount {
        return errors.New("insufficient funds")
    }

    // Now safe to execute
    return executeTransfer(from, to, amount)
}

4. Vague Error Messages

Failing fast but not explaining why.

# Bad: unhelpful error
[ -f "$CONFIG" ] || exit 1

# Good: actionable error message
[ -f "$CONFIG" ] || { echo "Config file not found: $CONFIG. Create it from config.example.yml"; exit 1; }

Implementation Checklist

Before implementing fail fast:

  • [ ] Identify all preconditions for the operation
  • [ ] Order validations by cost (cheapest first)
  • [ ] Validate before side effects (file creation, API calls, etc.)
  • [ ] Provide actionable error messages that help users fix the problem
  • [ ] Return appropriate error codes (HTTP 400 vs 500, exit 1 vs 2)
  • [ ] Log validation failures for debugging
  • [ ] Test invalid inputs explicitly in your test suite

Relationship to Other Patterns

Pattern How Fail Fast Applies
Graceful Degradation Complementary: fail fast on preconditions, degrade on runtime
Prerequisite Checks Specialized form of fail fast for complex preconditions
Idempotency Fail fast prevents partial state that breaks idempotency

Further Reading

Comments