Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Latest commit

 

History

History
1202 lines (1007 loc) · 36.8 KB

File metadata and controls

1202 lines (1007 loc) · 36.8 KB

Cause: A Real-World Guide

Table of Contents

  1. Introduction & Core Concepts
  2. Basic Usage Patterns
  3. Real-World Examples
  4. Advanced Features Deep Dive
  5. Practical Patterns & Best Practices
  6. Integration Examples

Introduction & Core Concepts

The Problem Cause Solves

Traditional error handling in JavaScript applications loses crucial failure information, making debugging and error analysis nearly impossible. When errors occur, you typically only see the final error state without understanding the complete failure history:

// Traditional approach - information loss and poor error composition
async function processOrder(orderId: string): Promise<Order> {
  try {
    const order = await fetchOrder(orderId);
    const payment = await processPayment(order.paymentInfo);
    const inventory = await updateInventory(order.items);
    return await finalizeOrder(order, payment, inventory);
  } catch (error) {
    // Lost information:
    // - Which specific step failed?
    // - Were there multiple failures?
    // - Was this a business logic error or system defect?
    // - Did the operation get interrupted?
    // - What was the complete failure chain?
    console.error('Order processing failed:', error.message);
    throw error; // Original context is lost
  }
}

// Concurrent operations - even worse information loss
async function processMultipleOrders(orderIds: string[]): Promise<Order[]> {
  try {
    const results = await Promise.allSettled(
      orderIds.map(id => processOrder(id))
    );
    
    // How do we meaningfully combine and report partial failures?
    const failures = results.filter(r => r.status === 'rejected');
    if (failures.length > 0) {
      // Lost: relationship between failures, timing, causality
      throw new Error(`Failed to process ${failures.length} orders`);
    }
    
    return results.map(r => (r as PromiseFulfilledResult<Order>).value);
  } catch (error) {
    // Complete loss of detailed failure information
    throw error;
  }
}

This approach leads to:

  • Information Loss - Original error context disappears in error chains
  • Poor Debuggability - No way to trace the complete failure history
  • Inadequate Error Classification - Cannot distinguish between business errors, system defects, and interruptions
  • Lost Causality - No understanding of how errors relate to each other in concurrent operations

The Cause Solution

Effect's Cause provides a lossless error model that captures the complete story of failures, preserving all error information for comprehensive analysis and debugging.

import { Cause, Effect } from "effect"

// Effect approach - complete failure information preserved
const processOrderEffect = (orderId: string) => Effect.gen(function* () {
  const order = yield* fetchOrderEffect(orderId)
  const payment = yield* processPaymentEffect(order.paymentInfo)
  const inventory = yield* updateInventoryEffect(order.items)
  return yield* finalizeOrderEffect(order, payment, inventory)
}).pipe(
  Effect.withSpan("process-order", { attributes: { orderId } })
)

// When this fails, the Cause contains:
// - Exact failure point with full context
// - Sequential relationship of operations  
// - Distinction between expected errors and defects
// - Interruption information if canceled
// - Complete stack trace and span information

Key Concepts

Cause Structure: A tree-like data structure that represents the complete failure history, including parallel and sequential error compositions.

Error Classification: Distinguishes between Fail (expected errors), Die (unexpected defects), and Interrupt (fiber cancellations).

Lossless Error Model: No failure information is ever discarded - every error, defect, and interruption is preserved with full context.

Basic Usage Patterns

Pattern 1: Accessing Cause Information

import { Cause, Effect } from "effect"

// Create effects that can fail in different ways
const businessLogicError = Effect.fail("User not found")
const systemDefect = Effect.die(new Error("Database connection failed"))
const interruptibleOperation = Effect.sleep("1 hour")

// Access the cause when an effect fails
const analyzeFailure = <E, A>(effect: Effect.Effect<A, E>) =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(effect)
    if (exit._tag === "Failure") {
      const cause = exit.cause
      console.log("Failure analysis:")
      console.log(`- Has business errors: ${Cause.isFailure(cause)}`)
      console.log(`- Has system defects: ${Cause.isDie(cause)}`)  
      console.log(`- Was interrupted: ${Cause.isInterrupted(cause)}`)
      console.log(`- Total error count: ${Cause.size(cause)}`)
      return { success: false, cause }
    }
    return { success: true, value: exit.value }
  })

Pattern 2: Cause Composition

import { Cause, Effect } from "effect"

// Sequential errors - when one error follows another
const sequentialFailures = Effect.gen(function* () {
  try {
    yield* Effect.fail("Primary operation failed")
  } finally {
    // Finalizer also fails - both errors are preserved
    yield* Effect.fail("Cleanup failed")  
  }
})

// Parallel errors - when multiple operations fail simultaneously  
const parallelFailures = Effect.all([
  Effect.fail("Service A failed"),
  Effect.fail("Service B failed"),
  Effect.fail("Service C failed")
], { concurrency: "unbounded" })

// Analyze composed failures
const analyzeComposedFailures = Effect.gen(function* () {
  const exit = yield* Effect.exit(parallelFailures)
  if (exit._tag === "Failure") {
    const cause = exit.cause
    const failures = Cause.failures(cause) // All business errors
    const defects = Cause.defects(cause)   // All system defects
    console.log(`Found ${failures.length} business errors`)
    console.log(`Found ${defects.length} system defects`)
  }
})

Pattern 3: Custom Error Analysis

import { Cause, Effect, Option } from "effect"

// Helper to extract specific error types from a cause
const findSpecificError = <E>(
  cause: Cause.Cause<E>,
  predicate: (error: E) => boolean
): Option.Option<E> => {
  return Cause.find(cause, (c) =>
    Cause.isFailType(c) && predicate(c.error) ? Option.some(c.error) : Option.none()
  )
}

// Extract and categorize errors
const categorizeErrors = <E>(cause: Cause.Cause<E>) => {
  const failures = Cause.failures(cause)
  const defects = Cause.defects(cause)
  const interruptions = Cause.interruptors(cause)
  
  return {
    businessErrors: failures,
    systemDefects: defects,
    cancellations: interruptions,
    isEmpty: Cause.isEmpty(cause),
    summary: Cause.pretty(cause)
  }
}

Real-World Examples

Example 1: E-commerce Order Processing with Comprehensive Error Tracking

import { Cause, Effect, Console } from "effect"

// Domain errors
class UserNotFoundError {
  readonly _tag = "UserNotFoundError"
  constructor(readonly userId: string) {}
}

class InsufficientInventoryError {
  readonly _tag = "InsufficientInventoryError"
  constructor(readonly productId: string, readonly requested: number, readonly available: number) {}
}

class PaymentFailedError {
  readonly _tag = "PaymentFailedError"
  constructor(readonly reason: string) {}
}

// Service layer with comprehensive error handling
const processOrderWithCauseTracking = (orderId: string) => Effect.gen(function* () {
  const order = yield* fetchOrder(orderId)
  const user = yield* validateUser(order.userId)
  const inventory = yield* checkInventory(order.items)
  const payment = yield* processPayment(order.total, user.paymentMethod)
  const result = yield* finalizeOrder(order, payment)
  
  return result
}).pipe(
  Effect.catchAllCause((cause) => Effect.gen(function* () {
    // Comprehensive error analysis and logging
    const errorReport = analyzeCause(cause)
    yield* Console.error("Order processing failed:", errorReport)
    
    // Different recovery strategies based on cause analysis
    if (Cause.isInterruptedOnly(cause)) {
      // Pure cancellation - no cleanup needed
      return yield* Effect.fail("Order cancelled by user")
    }
    
    if (hasPaymentErrors(cause)) {
      // Payment failed - initiate refund if needed
      yield* initiateRefundProcess(orderId)
    }
    
    if (hasInventoryErrors(cause)) {
      // Inventory issues - restore reserved items
      yield* restoreInventoryReservation(orderId)
    }
    
    // Re-throw with enriched context
    return yield* Effect.failCause(cause)
  })),
  Effect.withSpan("process-order", { attributes: { orderId } })
)

// Helper functions for cause analysis
const analyzeCause = (cause: Cause.Cause<unknown>) => {
  const failures = Cause.failures(cause)
  const defects = Cause.defects(cause)
  
  return {
    totalErrors: Cause.size(cause),
    businessErrors: failures.length,
    systemDefects: defects.length,
    wasInterrupted: Cause.isInterrupted(cause),
    primaryError: Cause.squash(cause),
    fullTrace: Cause.pretty(cause),
    errorTypes: categorizeBusinessErrors(failures)
  }
}

const categorizeBusinessErrors = (errors: readonly unknown[]) => {
  const categories = {
    userErrors: 0,
    inventoryErrors: 0,
    paymentErrors: 0,
    other: 0
  }
  
  errors.forEach(error => {
    if (error instanceof UserNotFoundError) {
      categories.userErrors++
    } else if (error instanceof InsufficientInventoryError) {
      categories.inventoryErrors++
    } else if (error instanceof PaymentFailedError) {
      categories.paymentErrors++
    } else {
      categories.other++
    }
  })
  
  return categories
}

const hasPaymentErrors = (cause: Cause.Cause<unknown>): boolean => {
  return Cause.find(cause, (c) =>
    Cause.isFailType(c) && c.error instanceof PaymentFailedError
      ? Option.some(true)
      : Option.none()
  ).pipe(Option.isSome)
}

Example 2: Microservices Communication with Parallel Error Handling

import { Cause, Effect, Array as Arr } from "effect"

// Service communication with detailed failure tracking
interface ServiceError {
  service: string
  operation: string
  cause: string
}

const callMultipleServices = (userId: string) => Effect.gen(function* () {
  // Call multiple services in parallel
  const results = yield* Effect.all([
    callUserService(userId).pipe(Effect.mapError(err => ({ service: "user", operation: "fetch", cause: err }))),
    callPreferencesService(userId).pipe(Effect.mapError(err => ({ service: "preferences", operation: "fetch", cause: err }))),  
    callBillingService(userId).pipe(Effect.mapError(err => ({ service: "billing", operation: "fetch", cause: err }))),
    callNotificationService(userId).pipe(Effect.mapError(err => ({ service: "notification", operation: "setup", cause: err })))
  ], { 
    concurrency: "unbounded",
    mode: "either" // Collect both successes and failures
  })
  
  return results
}).pipe(
  Effect.catchAllCause((cause) => Effect.gen(function* () {
    // Analyze parallel failures
    const failureAnalysis = analyzeParallelFailures(cause)
    
    yield* Console.error("Service communication analysis:", failureAnalysis)
    
    // Determine if we can continue with partial data
    if (failureAnalysis.criticalServicesDown.length === 0) {
      // Non-critical services failed - continue with degraded mode
      yield* Console.log("Continuing with degraded service mode")
      return yield* Effect.succeed("degraded-mode")
    }
    
    // Critical services failed - cannot continue
    return yield* Effect.fail(`Critical services unavailable: ${failureAnalysis.criticalServicesDown.join(", ")}`)
  }))
)

const analyzeParallelFailures = (cause: Cause.Cause<ServiceError>) => {
  const failures = Cause.failures(cause)
  const defects = Cause.defects(cause)
  
  const serviceFailures = failures.filter(f => typeof f === 'object' && f && 'service' in f) as ServiceError[]
  
  const criticalServices = ["user", "billing"]
  const criticalServicesDown = serviceFailures
    .filter(f => criticalServices.includes(f.service))
    .map(f => f.service)
    
  const nonCriticalServicesDown = serviceFailures
    .filter(f => !criticalServices.includes(f.service))  
    .map(f => f.service)
  
  return {
    totalFailures: Cause.size(cause),
    serviceFailures: serviceFailures.length,
    systemDefects: defects.length,
    criticalServicesDown,
    nonCriticalServicesDown,
    canContinueWithDegradedMode: criticalServicesDown.length === 0,
    detailedReport: Cause.pretty(cause)
  }
}

Example 3: Database Transaction with Rollback and Error Recovery

import { Cause, Effect, Console } from "effect"

// Database operations with comprehensive error tracking
class DatabaseConnectionError {
  readonly _tag = "DatabaseConnectionError"
  constructor(readonly message: string) {}
}

class TransactionError {
  readonly _tag = "TransactionError" 
  constructor(readonly operation: string, readonly cause: unknown) {}
}

class ValidationError {
  readonly _tag = "ValidationError"
  constructor(readonly field: string, readonly message: string) {}
}

const performComplexDatabaseTransaction = (operations: DatabaseOperation[]) => Effect.gen(function* () {
  const transaction = yield* beginTransaction()
  
  try {
    const results = []
    for (const operation of operations) {
      const result = yield* executeOperation(operation, transaction)
      results.push(result)  
    }
    
    yield* commitTransaction(transaction)
    return results
  } catch (error) {
    // Transaction failed - rollback
    yield* rollbackTransaction(transaction)
    throw error
  }
}).pipe(
  Effect.catchAllCause((cause) => Effect.gen(function* () {
    // Comprehensive transaction failure analysis
    const analysis = analyzeDatabaseFailure(cause)
    
    yield* Console.error("Database transaction failed:", analysis.summary)
    
    // Recovery strategies based on failure type
    if (analysis.hasConnectionErrors) {
      // Connection issues - retry with backoff
      yield* Console.log("Connection error detected - will retry")
      return yield* retryWithBackoff(performComplexDatabaseTransaction(operations))
    }
    
    if (analysis.hasValidationErrors) {
      // Data validation failed - return detailed errors for client
      const validationErrors = extractValidationErrors(cause)
      return yield* Effect.fail({
        type: "validation_failed",
        errors: validationErrors,
        recoverable: true
      })
    }
    
    if (analysis.hasTransactionConflicts) {
      // Optimistic locking conflicts - retry immediately  
      yield* Console.log("Transaction conflict - retrying")
      return yield* performComplexDatabaseTransaction(operations)
    }
    
    // Unknown database error - escalate
    yield* Console.error("Unknown database error - escalating")
    return yield* Effect.failCause(cause)
  })),
  Effect.withSpan("database-transaction")
)

const analyzeDatabaseFailure = (cause: Cause.Cause<unknown>) => {
  const failures = Cause.failures(cause)
  const defects = Cause.defects(cause)
  
  const hasConnectionErrors = failures.some(f => f instanceof DatabaseConnectionError)
  const hasValidationErrors = failures.some(f => f instanceof ValidationError)
  const hasTransactionConflicts = failures.some(f => 
    f instanceof TransactionError && f.cause && 
    typeof f.cause === 'string' && f.cause.includes('conflict')
  )
  
  return {
    totalFailures: Cause.size(cause),
    hasConnectionErrors,
    hasValidationErrors, 
    hasTransactionConflicts,
    hasSystemDefects: defects.length > 0,
    wasInterrupted: Cause.isInterrupted(cause),
    summary: {
      errorCount: failures.length,
      defectCount: defects.length,
      primaryError: Cause.squash(cause),
      fullTrace: Cause.pretty(cause)
    }
  }
}

const extractValidationErrors = (cause: Cause.Cause<unknown>): ValidationError[] => {
  const failures = Cause.failures(cause)
  return failures.filter(f => f instanceof ValidationError) as ValidationError[]
}

Advanced Features Deep Dive

Feature 1: Cause Transformation and Mapping

Transform error types within causes while preserving the complete failure structure.

Basic Cause Mapping

import { Cause, Effect } from "effect"

// Transform specific error types in a cause
const mapBusinessErrors = <E1, E2>(
  cause: Cause.Cause<E1>,
  f: (error: E1) => E2
): Cause.Cause<E2> => {
  return Cause.map(cause, f)
}

// Example: Sanitize errors for external API
const sanitizeErrorsForClient = (cause: Cause.Cause<DatabaseError>) => {
  return Cause.map(cause, (error) => {
    switch (error._tag) {
      case "DatabaseConnectionError":
        return { type: "service_unavailable", message: "Service temporarily unavailable" }
      case "ValidationError":
        return { type: "validation_error", field: error.field, message: error.message }
      default:
        return { type: "internal_error", message: "An unexpected error occurred" }
    }
  })
}

Advanced Cause Composition

import { Cause, Effect } from "effect"

// Combine causes with custom logic
const combineCausesWithPriority = <E>(
  primary: Cause.Cause<E>, 
  secondary: Cause.Cause<E>
): Cause.Cause<E> => {
  // Primary errors take precedence in sequential composition
  if (Cause.isEmpty(primary)) return secondary
  if (Cause.isEmpty(secondary)) return primary
  return Cause.sequential(primary, secondary)
}

// FlatMap causes for complex error transformations
const enrichCauseWithContext = <E>(
  cause: Cause.Cause<E>,
  contextProvider: (error: E) => Cause.Cause<E>
): Cause.Cause<E> => {
  return Cause.flatMap(cause, (error) => {
    const contextCause = contextProvider(error)
    return Cause.sequential(Cause.fail(error), contextCause)
  })
}

// Usage example
const addErrorContext = (error: ServiceError) => {
  return Cause.fail({
    ...error,
    timestamp: new Date().toISOString(),
    context: "service-layer",
    severity: error.retryable ? "warning" : "error"
  })
}

Feature 2: Cause Analysis and Inspection

Deep inspection and analysis of cause structures for monitoring and debugging.

Comprehensive Cause Inspection

import { Cause, Effect, Array as Arr, HashSet } from "effect"

// Deep cause analysis utility
const analyzeCauseStructure = <E>(cause: Cause.Cause<E>) => {
  const analysis = {
    structure: {
      size: Cause.size(cause),
      isEmpty: Cause.isEmpty(cause),
      hasFailures: Cause.isFailure(cause),
      hasDefects: Cause.isDie(cause),
      hasInterruptions: Cause.isInterrupted(cause),
      isInterruptedOnly: Cause.isInterruptedOnly(cause)
    },
    content: {
      failures: Cause.failures(cause),
      defects: Cause.defects(cause),
      interruptors: Cause.interruptors(cause)
    },
    firstOccurrences: {
      firstFailure: Cause.failureOption(cause),
      firstDefect: Cause.dieOption(cause),
      firstInterruption: Cause.interruptOption(cause)
    },
    formatted: {
      prettyPrint: Cause.pretty(cause),
      prettyErrors: Cause.prettyErrors(cause)
    }
  }
  
  return analysis
}

// Search and filter causes
const findCriticalErrors = <E>(
  cause: Cause.Cause<E>,
  isCritical: (error: E) => boolean
) => {
  return Cause.find(cause, (c) => {
    if (Cause.isFailType(c) && isCritical(c.error)) {
      return Option.some(c.error)
    }
    return Option.none()
  })
}

// Extract specific error patterns
const extractErrorPatterns = <E>(cause: Cause.Cause<E>) => {
  const patterns = {
    sequentialErrors: [] as Array<{ left: E, right: E }>,
    parallelErrors: [] as Array<{ left: E[], right: E[] }>,
    isolatedErrors: [] as E[]
  }
  
  const visitor = (c: Cause.Cause<E>): void => {
    if (Cause.isSequentialType(c)) {
      const leftFailures = Cause.failures(c.left)
      const rightFailures = Cause.failures(c.right)
      if (leftFailures.length === 1 && rightFailures.length === 1) {
        patterns.sequentialErrors.push({
          left: leftFailures[0],
          right: rightFailures[0]
        })
      }
      visitor(c.left)
      visitor(c.right)
    } else if (Cause.isParallelType(c)) {
      patterns.parallelErrors.push({
        left: Cause.failures(c.left),
        right: Cause.failures(c.right)
      })
      visitor(c.left)
      visitor(c.right)
    } else if (Cause.isFailType(c)) {
      patterns.isolatedErrors.push(c.error)
    }
  }
  
  visitor(cause)
  return patterns
}

Real-World Cause Analysis: Error Monitoring Integration

import { Cause, Effect, Console } from "effect"

// Production error monitoring integration
const reportCauseToMonitoring = <E>(
  cause: Cause.Cause<E>,
  context: { operation: string; userId?: string; requestId?: string }
) => Effect.gen(function* () {
  const analysis = analyzeCauseStructure(cause)
  
  // Send structured error data to monitoring service
  const errorReport = {
    timestamp: new Date().toISOString(),
    operation: context.operation,
    userId: context.userId,
    requestId: context.requestId,
    error: {
      type: determineErrorType(cause),
      severity: determineSeverity(cause),
      count: analysis.structure.size,
      retryable: isRetryable(cause),
      primaryError: Cause.squash(cause),
      fullCause: Cause.pretty(cause),
      errorBreakdown: {
        businessErrors: analysis.content.failures.length,
        systemDefects: analysis.content.defects.length,
        interruptions: analysis.content.interruptors.size
      }
    }
  }
  
  // Log based on severity
  if (errorReport.error.severity === "critical") {
    yield* Console.error("CRITICAL ERROR:", errorReport)
    // Send alert to on-call team
    yield* sendCriticalAlert(errorReport)
  } else if (errorReport.error.severity === "error") {
    yield* Console.error("ERROR:", errorReport)
  } else {
    yield* Console.warn("WARNING:", errorReport)
  }
  
  // Store for analytics
  yield* storeErrorMetrics(errorReport)
})

const determineErrorType = <E>(cause: Cause.Cause<E>): string => {
  if (Cause.isInterruptedOnly(cause)) return "cancellation"
  if (Cause.isDie(cause) && !Cause.isFailure(cause)) return "system_defect"
  if (Cause.isFailure(cause) && !Cause.isDie(cause)) return "business_error"
  return "mixed_failure"
}

const determineSeverity = <E>(cause: Cause.Cause<E>): "critical" | "error" | "warning" => {
  if (Cause.isDie(cause)) return "critical"
  if (Cause.size(cause) > 3) return "error"
  return "warning"
}

const isRetryable = <E>(cause: Cause.Cause<E>): boolean => {
  // Generally, system defects are not retryable, but business errors might be
  return !Cause.isDie(cause) || Cause.isInterrupted(cause)
}

Feature 3: Custom Cause Reducers

Build sophisticated cause analysis with custom reduction strategies.

Advanced Cause Reduction

import { Cause, FiberId, Option } from "effect"

// Custom reducer for error aggregation
const errorAggregationReducer: Cause.CauseReducer<
  { operationName: string }, 
  ServiceError, 
  ErrorSummary
> = {
  emptyCase: (context) => ({
    operation: context.operationName,
    totalErrors: 0,
    errorsByType: {},
    severity: "none" as const,
    retryable: true
  }),
  
  failCase: (context, error) => ({
    operation: context.operationName,
    totalErrors: 1,
    errorsByType: { [error._tag]: 1 },
    severity: error.severity,
    retryable: error.retryable
  }),
  
  dieCase: (context, defect) => ({
    operation: context.operationName,
    totalErrors: 1,
    errorsByType: { "SystemDefect": 1 },
    severity: "critical" as const,
    retryable: false
  }),
  
  interruptCase: (context, fiberId) => ({
    operation: context.operationName,
    totalErrors: 1,
    errorsByType: { "Interruption": 1 },
    severity: "info" as const,
    retryable: true
  }),
  
  sequentialCase: (context, left, right) => ({
    operation: context.operationName,
    totalErrors: left.totalErrors + right.totalErrors,
    errorsByType: mergeErrorCounts(left.errorsByType, right.errorsByType),
    severity: maxSeverity(left.severity, right.severity),
    retryable: left.retryable && right.retryable
  }),
  
  parallelCase: (context, left, right) => ({
    operation: context.operationName,
    totalErrors: left.totalErrors + right.totalErrors,
    errorsByType: mergeErrorCounts(left.errorsByType, right.errorsByType),
    severity: maxSeverity(left.severity, right.severity),
    retryable: left.retryable || right.retryable
  })
}

interface ErrorSummary {
  operation: string
  totalErrors: number
  errorsByType: Record<string, number>
  severity: "none" | "info" | "warning" | "error" | "critical"
  retryable: boolean
}

// Usage of custom reducer
const summarizeErrors = (
  cause: Cause.Cause<ServiceError>,
  operationName: string
): ErrorSummary => {
  return Cause.reduceWithContext(
    cause,
    { operationName },
    errorAggregationReducer
  )
}

// Helper functions
const mergeErrorCounts = (
  a: Record<string, number>,
  b: Record<string, number>
): Record<string, number> => {
  const result = { ...a }
  for (const [key, count] of Object.entries(b)) {
    result[key] = (result[key] || 0) + count
  }
  return result
}

const maxSeverity = (
  a: ErrorSummary["severity"],
  b: ErrorSummary["severity"]
): ErrorSummary["severity"] => {
  const severityOrder = ["none", "info", "warning", "error", "critical"]
  const aIndex = severityOrder.indexOf(a)
  const bIndex = severityOrder.indexOf(b)
  return severityOrder[Math.max(aIndex, bIndex)] as ErrorSummary["severity"]
}

Practical Patterns & Best Practices

Pattern 1: Defensive Error Analysis

import { Cause, Effect, Option } from "effect"

// Safe cause analysis that handles all edge cases
const safeCauseAnalysis = <E>(cause: Cause.Cause<E>) => {
  const safeExtractFirstError = (): Option.Option<E> => {
    try {
      return Cause.failureOption(cause)
    } catch {
      return Option.none()
    }
  }
  
  const safeGetDefects = () => {
    try {
      return Cause.defects(cause)
    } catch {
      return []
    }
  }
  
  const safePrettyPrint = (): string => {
    try {
      return Cause.pretty(cause)
    } catch (error) {
      return `[Error formatting cause: ${String(error)}]`
    }
  }
  
  return {
    firstError: safeExtractFirstError(),
    defects: safeGetDefects(),
    formatted: safePrettyPrint(),
    isEmpty: Cause.isEmpty(cause),
    size: Cause.size(cause)
  }
}

// Robust error categorization
const categorizeCause = <E>(cause: Cause.Cause<E>) => {
  const analysis = safeCauseAnalysis(cause)
  
  if (analysis.isEmpty) {
    return { category: "success", priority: 0, actionable: false }
  }
  
  if (Cause.isInterruptedOnly(cause)) {
    return { category: "cancellation", priority: 1, actionable: false }
  }
  
  if (Cause.isDie(cause) && !Cause.isFailure(cause)) {
    return { category: "system_failure", priority: 4, actionable: true }
  }
  
  if (Cause.isFailure(cause) && !Cause.isDie(cause)) {
    return { category: "business_error", priority: 2, actionable: true }
  }
  
  return { category: "mixed_failure", priority: 3, actionable: true }
}

Pattern 2: Error Recovery Based on Cause Analysis

import { Cause, Effect, Schedule } from "effect"

// Smart retry logic based on cause analysis
const retryWithCauseAnalysis = <A, E, R>(
  effect: Effect.Effect<A, E, R>,
  maxRetries: number = 3
) => {
  const shouldRetryForCause = (cause: Cause.Cause<E>): boolean => {
    // Never retry system defects
    if (Cause.isDie(cause)) return false
    
    // Don't retry interruptions
    if (Cause.isInterruptedOnly(cause)) return false
    
    // Retry business errors based on specific error types
    const failures = Cause.failures(cause)
    return failures.some(isRetryableError)
  }
  
  const getRetryDelayForCause = (cause: Cause.Cause<E>, attempt: number): number => {
    if (Cause.isFailure(cause)) {
      // Exponential backoff for business errors
      return Math.min(1000 * Math.pow(2, attempt), 10000)
    }
    // Immediate retry for other cases
    return 0
  }
  
  return effect.pipe(
    Effect.retryN(maxRetries),
    Effect.retry(
      Schedule.recurWhile((cause: Cause.Cause<E>) => shouldRetryForCause(cause))
        .pipe(Schedule.compose(Schedule.exponential("1 second")))
    )
  )
}

const isRetryableError = (error: unknown): boolean => {
  // Define retryable error types based on your domain
  return (
    typeof error === 'object' &&
    error !== null &&
    'retryable' in error &&
    error.retryable === true
  )
}

Pattern 3: Cause-Aware Logging and Metrics

import { Cause, Effect, Console, Metrics } from "effect"

// Structured logging based on cause analysis
const logWithCauseAnalysis = <E>(
  cause: Cause.Cause<E>,
  context: { operation: string; level: "debug" | "info" | "warn" | "error" }
) => Effect.gen(function* () {
  const analysis = analyzeCauseStructure(cause)
  
  const logEntry = {
    timestamp: new Date().toISOString(),
    operation: context.operation,
    level: context.level,
    error: {
      type: analysis.structure.hasFailures ? "business" : "system",
      count: analysis.structure.size,
      retryable: !analysis.structure.hasDefects,
      details: {
        failures: analysis.content.failures.length,
        defects: analysis.content.defects.length,
        interruptions: analysis.content.interruptors.size
      },
      message: analysis.formatted.prettyPrint
    }
  }
  
  // Log based on error characteristics
  if (analysis.structure.hasDefects) {
    yield* Console.error("SYSTEM DEFECT:", logEntry)
  } else if (analysis.structure.hasFailures) {
    yield* Console.warn("BUSINESS ERROR:", logEntry)  
  } else if (analysis.structure.hasInterruptions) {
    yield* Console.info("OPERATION CANCELLED:", logEntry)
  }
  
  // Update metrics
  yield* updateErrorMetrics(analysis)
})

// Metrics collection based on cause analysis
const updateErrorMetrics = <E>(analysis: ReturnType<typeof analyzeCauseStructure<E>>) => Effect.gen(function* () {
  const errorTypeCounter = Metrics.counter("errors_by_type")
  const errorSizeHistogram = Metrics.histogram("error_cause_size")
  
  if (analysis.structure.hasFailures) {
    yield* errorTypeCounter.increment({ type: "business" })
  }
  
  if (analysis.structure.hasDefects) {
    yield* errorTypeCounter.increment({ type: "system" })
  }
  
  if (analysis.structure.hasInterruptions) {
    yield* errorTypeCounter.increment({ type: "interruption" })
  }
  
  yield* errorSizeHistogram.update(analysis.structure.size)
})

Integration Examples

Integration with Popular Monitoring Services

import { Cause, Effect, Console } from "effect"

// Integration with Datadog/New Relic style monitoring
interface MonitoringService {
  recordError(error: {
    message: string
    level: "info" | "warning" | "error" | "critical"
    tags: Record<string, string>
    context: Record<string, unknown>
  }): Effect.Effect<void>
  
  recordMetric(name: string, value: number, tags?: Record<string, string>): Effect.Effect<void>
}

const integrateWithMonitoring = (monitoring: MonitoringService) => {
  const reportCause = <E>(
    cause: Cause.Cause<E>,
    operation: string,
    additionalContext: Record<string, unknown> = {}
  ) => Effect.gen(function* () {
    const analysis = analyzeCauseStructure(cause)
    
    // Determine monitoring level
    const level = analysis.structure.hasDefects ? "critical" :
                  analysis.structure.hasFailures ? "error" :
                  analysis.structure.hasInterruptions ? "warning" : "info"
    
    // Create structured error report
    yield* monitoring.recordError({
      message: analysis.formatted.prettyPrint,
      level,
      tags: {
        operation,
        error_type: analysis.structure.hasDefects ? "defect" : "failure",
        retryable: String(!analysis.structure.hasDefects),
        interrupted: String(analysis.structure.hasInterruptions)
      },
      context: {
        ...additionalContext,
        error_count: analysis.structure.size,
        failures: analysis.content.failures.length,
        defects: analysis.content.defects.length,
        interruptions: analysis.content.interruptors.size
      }
    })
    
    // Record metrics
    yield* monitoring.recordMetric("errors.total", 1, { operation, type: "count" })
    yield* monitoring.recordMetric("errors.size", analysis.structure.size, { operation, type: "histogram" })
  })
  
  return { reportCause }
}

Integration with Testing Frameworks

import { Cause, Effect, TestContext } from "effect"

// Test utilities for cause-based assertions
const expectCauseToMatch = <E>(
  cause: Cause.Cause<E>,
  matcher: {
    hasFailures?: boolean
    hasDefects?: boolean  
    hasInterruptions?: boolean
    errorCount?: number
    containsError?: (error: E) => boolean
  }
) => {
  const analysis = analyzeCauseStructure(cause)
  
  if (matcher.hasFailures !== undefined) {
    expect(analysis.structure.hasFailures).toBe(matcher.hasFailures)
  }
  
  if (matcher.hasDefects !== undefined) {
    expect(analysis.structure.hasDefects).toBe(matcher.hasDefects)
  }
  
  if (matcher.hasInterruptions !== undefined) {
    expect(analysis.structure.hasInterruptions).toBe(matcher.hasInterruptions)
  }
  
  if (matcher.errorCount !== undefined) {
    expect(analysis.structure.size).toBe(matcher.errorCount)
  }
  
  if (matcher.containsError) {
    const hasMatchingError = analysis.content.failures.some(matcher.containsError)
    expect(hasMatchingError).toBe(true)
  }
}

// Test suite example
describe("Order Processing Error Handling", () => {
  it("should handle payment failures with proper cause structure", async () => {
    const program = processOrder("order-123").pipe(
      Effect.exit
    )
    
    const result = await Effect.runPromise(program)
    
    if (result._tag === "Failure") {
      expectCauseToMatch(result.cause, {
        hasFailures: true,
        hasDefects: false,
        errorCount: 1,
        containsError: (error) => error instanceof PaymentFailedError
      })
    }
  })
  
  it("should compose parallel errors correctly", async () => {
    const program = Effect.all([
      Effect.fail("Error A"),
      Effect.fail("Error B"),
      Effect.fail("Error C")
    ], { concurrency: "unbounded" }).pipe(
      Effect.exit
    )
    
    const result = await Effect.runPromise(program)
    
    if (result._tag === "Failure") {
      expectCauseToMatch(result.cause, {
        hasFailures: true,
        errorCount: 3
      })
      
      // Verify all errors are present
      const failures = Cause.failures(result.cause)
      expect(failures).toContain("Error A")
      expect(failures).toContain("Error B") 
      expect(failures).toContain("Error C")
    }
  })
})

Integration with Observability Tools

import { Cause, Effect, Tracer } from "effect"

// OpenTelemetry integration for distributed tracing
const addCauseToTracing = <E>(
  cause: Cause.Cause<E>,
  span: Tracer.Span
) => Effect.gen(function* () {
  const analysis = analyzeCauseStructure(cause)
  
  // Add cause information to the current span
  span.addEvent("error_occurred", {
    "error.type": analysis.structure.hasDefects ? "defect" : "failure",
    "error.count": analysis.structure.size,
    "error.retryable": !analysis.structure.hasDefects,
    "error.has_failures": analysis.structure.hasFailures,
    "error.has_defects": analysis.structure.hasDefects,
    "error.has_interruptions": analysis.structure.hasInterruptions
  })
  
  // Set span status based on cause
  if (analysis.structure.hasDefects) {
    span.setStatus({ code: "error", message: "System defect occurred" })
  } else if (analysis.structure.hasFailures) {
    span.setStatus({ code: "error", message: "Business error occurred" })
  }
  
  // Add detailed error information
  span.setAttribute("error.message", analysis.formatted.prettyPrint)
  span.setAttribute("error.primary", String(Cause.squash(cause)))
})

// Usage in traced operations
const tracedOperationWithCauseIntegration = (operationId: string) => Effect.gen(function* () {
  const result = yield* riskyOperation(operationId).pipe(
    Effect.withSpan("risky-operation", { attributes: { operationId } }),
    Effect.tapErrorCause((cause) => Effect.gen(function* () {
      const span = yield* Effect.serviceWith(Tracer.TracerTypeId, tracer => tracer.getCurrentSpan())
      if (span) {
        yield* addCauseToTracing(cause, span)
      }
    }))
  )
  
  return result
})

Conclusion

Cause provides comprehensive failure analysis and lossless error handling for Effect programs, enabling precise debugging, intelligent error recovery, and sophisticated monitoring integration.

Key benefits:

  • Complete Error History: Never lose failure information with comprehensive cause tracking
  • Intelligent Error Classification: Distinguish between business errors, system defects, and interruptions
  • Rich Failure Analysis: Deep inspection capabilities for debugging and monitoring
  • Composable Error Handling: Combine and transform causes while preserving all failure information
  • Advanced Recovery Strategies: Make informed decisions based on complete failure context

Use Cause when you need to understand not just what went wrong, but exactly how and why your Effect programs fail, enabling you to build more resilient and debuggable applications.