- Introduction & Core Concepts
- Basic Usage Patterns
- Real-World Examples
- Advanced Features Deep Dive
- Practical Patterns & Best Practices
- Integration Examples
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
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 informationCause 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.
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 }
})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`)
}
})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)
}
}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)
}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)
}
}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[]
}Transform error types within causes while preserving the complete failure structure.
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" }
}
})
}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"
})
}Deep inspection and analysis of cause structures for monitoring and debugging.
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
}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)
}Build sophisticated cause analysis with custom reduction strategies.
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"]
}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 }
}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
)
}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)
})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 }
}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")
}
})
})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
})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.