Effect-TS Best Practices
Opinionated patterns for Effect-TS codebases. Effect provides typed functional programming with composable errors, dependency injection, and observability.
Critical Rules
-
NEVER use any or type casts (as Type ) - Use Schema.make() for branded types, Schema.decodeUnknown() for parsing
-
Don't use catchAll when error type is never
-
No errors to catch
-
Never use global Error in Effect channels - Use Schema.TaggedError for domain errors
-
Ban { disableValidation: true }
-
Lint against this
-
Don't wrap safe operations in Effect - Only use Effect.try() for throwing operations
-
Use mapError not catchAllCause
-
Distinguish expected errors from bugs
-
Never silently swallow errors - Failures MUST be visible in the Effect's error channel E
Quick Reference
Pattern DON'T DO
Service definition Context.Tag
Effect.Service with dependencies array
Error types Generic Error
Schema.TaggedError with context fields
Branded IDs Raw string
Schema.String.pipe(Schema.brand("@Ns/Entity"))
Running effects runSync /runPromise in services Return Effect , run at edge
Logging console.log
Effect.log with structured data
Configuration process.env
Config with validation
Method tracing Manual spans Effect.fn("Service.method")
Nullable results null /undefined
Option types
State Mutable variables Ref
Time Date.now() , new Date()
Clock service
Service Pattern
class UserService extends Effect.Service<UserService>()("UserService", { dependencies: [DatabaseService.Default], effect: Effect.gen(function* () { const db = yield* DatabaseService
return {
findById: Effect.fn("UserService.findById")(
(id: UserId) => db.query(/* ... */)
),
}
}), }) {}
// Usage - dependencies auto-provided UserService.findById(userId)
Error Handling
// Define domain-specific errors class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()( "UserNotFoundError", { userId: UserId, message: Schema.String } ) {}
// Handle with catchTag (preserves type info) effect.pipe( Effect.catchTag("UserNotFoundError", (e) => /* handle /), Effect.catchTag("AuthExpiredError", (e) => / handle */) )
Schema Pattern
// Branded ID const UserId = Schema.String.pipe(Schema.brand("@App/UserId"))
// Domain entity with Schema.Class class User extends Schema.Class<User>("User")({ id: UserId, email: Schema.String, createdAt: Schema.DateFromSelf, }) { get displayName() { return this.email.split("@")[0] } }
Layer Composition
// Declare dependencies in service, not at usage const MainLayer = Layer.mergeAll( UserServiceLive, AuthServiceLive, DatabaseLive )
// Run program Effect.runPromise(program.pipe(Effect.provide(MainLayer)))
Detailed Guides
-
Anti-Patterns - Forbidden patterns with fixes
-
Error Patterns - Domain errors, rich context, HTTP mapping
-
Schema Patterns - Branded types, transforms, validation
-
Service Patterns - Effect.Service, dependency injection
-
Layer Patterns - Composition, memoization, testing
-
Observability Patterns - Logging, tracing, metrics
-
SQL Patterns - Database integration, transactions
-
Testing Patterns - effect-vitest, property testing, Testcontainers
-
Atom Patterns - React state management with Effect
-
RPC & Cluster Patterns - Distributed systems