Effect Core Patterns
Master the core Effect patterns for building type-safe, composable applications with Effect. This skill covers the Effect type, constructors, and composition patterns using Effect.gen.
The Effect Type
The Effect type has three type parameters:
Effect<Success, Error, Requirements>
-
Success (A): The type of value that an effect can succeed with
-
Error (E): The expected errors that can occur (use never for no errors)
-
Requirements (R): The contextual dependencies required (use never for no dependencies)
import { Effect } from "effect"
// Effect that succeeds with number, never fails, no requirements const simpleEffect: Effect.Effect<number, never, never> = Effect.succeed(42)
// Effect that can fail with string error const failableEffect: Effect.Effect<number, string, never> = Effect.fail("Something went wrong")
// Effect that requires a UserService interface UserService { getUser: (id: string) => Effect.Effect<User, DbError, never> }
const effectWithDeps: Effect.Effect<User, DbError, UserService> = Effect.gen(function* () { const userService = yield* Effect.service(UserService) const user = yield* userService.getUser("123") return user })
Creating Effects
Effect.succeed - Always Succeeds
Use when you have a pure value and need an Effect:
import { Effect } from "effect"
const result = Effect.succeed(42) // Effect<number, never, never>
const user = Effect.succeed({ id: "1", name: "Alice" }) // Effect<User, never, never>
// Void effect (produces no useful value) const voidEffect = Effect.succeed(undefined) // Effect<void, never, never>
Effect.fail - Expected Failure
Use for recoverable, expected errors:
import { Effect } from "effect"
interface ValidationError { _tag: "ValidationError" message: string }
const validateAge = (age: number): Effect.Effect<number, ValidationError, never> => { if (age < 0) { return Effect.fail({ _tag: "ValidationError", message: "Age must be positive" }) } return Effect.succeed(age) }
// Usage with Effect.gen const program = Effect.gen(function* () { const age = yield* validateAge(-5) // This will fail return age })
Effect.sync - Synchronous Side Effects
Use for synchronous operations with side effects:
import { Effect } from "effect"
// Reading from a mutable variable let counter = 0
const incrementCounter = Effect.sync(() => { counter++ return counter })
// Logging const log = (message: string) => Effect.sync(() => { console.log(message) })
// Current timestamp const now = Effect.sync(() => Date.now())
// IMPORTANT: The function should not throw // Thrown errors become "defects" (unexpected failures)
Effect.try - Synchronous Operations That May Fail
Use for sync operations that might throw:
import { Effect } from "effect"
// Parse JSON safely const parseJSON = (text: string): Effect.Effect<unknown, Error, never> => Effect.try(() => JSON.parse(text))
// With custom error mapping interface ParseError { _tag: "ParseError" message: string }
const parseJSONCustom = (text: string): Effect.Effect<unknown, ParseError, never> => Effect.try({ try: () => JSON.parse(text), catch: (error) => ({ _tag: "ParseError", message: error instanceof Error ? error.message : String(error) }) })
// Usage const program = Effect.gen(function* () { const data = yield* parseJSON('{"name": "Alice"}') return data })
Effect.promise - Async Operations (No Errors)
Use for promises that should never reject:
import { Effect } from "effect"
// Delayed execution const delay = (ms: number): Effect.Effect<void, never, never> => Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms)) )
// Fetch with assumption it won't fail const fetchData = (url: string): Effect.Effect<Response, never, never> => Effect.promise(() => fetch(url))
// IMPORTANT: If promise rejects, it becomes a "defect" // Use Effect.tryPromise for operations that can fail
Effect.tryPromise - Async Operations That May Fail
Use for promises that might reject:
import { Effect } from "effect"
interface NetworkError { _tag: "NetworkError" message: string statusCode?: number }
const fetchUser = (id: string): Effect.Effect<User, NetworkError, never> =>
Effect.tryPromise({
try: async () => {
const response = await fetch(/api/users/${id})
if (!response.ok) {
throw new Error(HTTP ${response.status})
}
return response.json()
},
catch: (error) => ({
_tag: "NetworkError",
message: error instanceof Error ? error.message : String(error),
statusCode: error instanceof Error && 'status' in error
? (error as any).status
: undefined
})
})
// Simplified version (errors become UnknownException)
const fetchUserSimple = (id: string): Effect.Effect<User, UnknownException, never> =>
Effect.tryPromise(() => fetch(/api/users/${id}).then(r => r.json()))
Effect.async - Callback-Based APIs
Use for wrapping callback-style APIs:
import { Effect } from "effect"
// Wrap setTimeout const sleep = (ms: number): Effect.Effect<void, never, never> => Effect.async<void>((resume) => { const timeoutId = setTimeout(() => { resume(Effect.succeed(undefined)) }, ms)
// Optional cleanup on interruption
return Effect.sync(() => {
clearTimeout(timeoutId)
})
})
// Wrap Node.js callback API interface FileError { _tag: "FileError" message: string }
const readFile = (path: string): Effect.Effect<string, FileError, never> => Effect.async<string, FileError>((resume) => { fs.readFile(path, 'utf8', (error, data) => { if (error) { resume(Effect.fail({ _tag: "FileError", message: error.message })) } else { resume(Effect.succeed(data)) } }) })
Composing Effects with Effect.gen
Effect.gen allows you to write effect code using generator syntax:
import { Effect } from "effect"
// Basic composition const program = Effect.gen(function* () { const a = yield* Effect.succeed(10) const b = yield* Effect.succeed(20) return a + b })
// With error handling const programWithErrors = Effect.gen(function* () { const age = yield* validateAge(25) const user = yield* createUser({ age }) return user })
// Sequential operations const fetchUserProfile = (userId: string) => Effect.gen(function* () { const user = yield* fetchUser(userId) const posts = yield* fetchPosts(user.id) const comments = yield* fetchComments(user.id) return { user, posts, comments } })
// Using control flow const processData = (data: unknown) => Effect.gen(function* () { const validated = yield* validateData(data)
if (validated.type === "user") {
const user = yield* createUser(validated)
return { type: "user", user }
} else {
const post = yield* createPost(validated)
return { type: "post", post }
}
})
// Error handling with short-circuiting const safeDivide = (a: number, b: number) => Effect.gen(function* () { if (b === 0) { yield* Effect.fail({ _tag: "DivideByZero" }) return // Explicit return for type narrowing } return a / b })
Running Effects
Effect.runSync - Synchronous Execution
Use for effects with no async operations or requirements:
import { Effect } from "effect"
const result = Effect.runSync(Effect.succeed(42)) // 42
// Throws if effect can fail try { Effect.runSync(Effect.fail("error")) } catch (error) { // Caught }
// CANNOT use with async effects or requirements // Effect.runSync(Effect.promise(() => fetch("..."))) // Runtime error!
Effect.runPromise - Async Execution
Use for async effects without requirements:
import { Effect } from "effect"
const program = Effect.gen(function* () { yield* delay(1000) return "Done" })
const result = await Effect.runPromise(program) // "Done" after 1 second
// Rejects on failure try { await Effect.runPromise(Effect.fail("error")) } catch (error) { // error === "error" }
Effect.runPromiseExit - Get Full Exit Information
Use when you need detailed success/failure information:
import { Effect, Exit } from "effect"
const program = Effect.succeed(42)
const exit = await Effect.runPromiseExit(program)
if (Exit.isSuccess(exit)) { console.log("Success:", exit.value) } else if (Exit.isFailure(exit)) { console.log("Failure:", exit.cause) }
Building Pipelines
Effect.map - Transform Success Values
import { Effect, pipe } from "effect"
const double = (n: number) => n * 2
// Using pipe const result = pipe( Effect.succeed(21), Effect.map(double) ) // Effect<42, never, never>
// Using method const result2 = Effect.succeed(21).pipe( Effect.map(double) )
// Chaining transformations const program = pipe( Effect.succeed("hello"), Effect.map(s => s.toUpperCase()), Effect.map(s => s.length) ) // Effect<5, never, never>
Effect.flatMap - Chain Dependent Effects
import { Effect, pipe } from "effect"
const getUser = (id: string): Effect.Effect<User, DbError, never> => { // ... }
const getUserPosts = (userId: string): Effect.Effect<Post[], DbError, never> => { // ... }
// Using pipe const program = pipe( getUser("123"), Effect.flatMap(user => getUserPosts(user.id)) )
// Using Effect.gen (more readable) const program2 = Effect.gen(function* () { const user = yield* getUser("123") const posts = yield* getUserPosts(user.id) return posts })
Effect.andThen - Sequential Composition
import { Effect, pipe } from "effect"
// Chain effects, ignoring previous result const program = pipe( log("Starting..."), Effect.andThen(processData()), Effect.andThen(log("Done!")) )
// Provide value to next effect const program2 = pipe( Effect.succeed(5), Effect.andThen(n => Effect.succeed(n * 2)) )
Effect.tap - Side Effects Without Changing Value
import { Effect, pipe } from "effect"
const program = pipe(
fetchUser("123"),
Effect.tap(user => log(Fetched user: ${user.name})),
Effect.tap(user => saveToCache(user)),
Effect.map(user => user.email)
)
// The taps run but don't change the flowing value
Effect Transformations
Effect.mapError - Transform Errors
import { Effect, pipe } from "effect"
interface DbError { _tag: "DbError" message: string }
interface AppError { _tag: "AppError" message: string context: string }
const program = pipe( queryDatabase(), Effect.mapError((dbError: DbError): AppError => ({ _tag: "AppError", message: dbError.message, context: "user-service" })) )
Effect.mapBoth - Transform Success and Error
import { Effect, pipe } from "effect"
const program = pipe( Effect.succeed(10), Effect.mapBoth({ onSuccess: (n) => n * 2, onFailure: (e) => ({ _tag: "MappedError", original: e }) }) )
Effect.orElse - Fallback on Failure
import { Effect, pipe } from "effect"
const program = pipe( fetchFromPrimaryDb(), Effect.orElse(() => fetchFromSecondaryDb()) )
// Fallback to different effect based on error const programWithCheck = pipe( riskyOperation(), Effect.orElse((error) => error._tag === "Timeout" ? retryOperation() : Effect.fail(error) ) )
Best Practices
Use Effect.gen for Readability: Prefer Effect.gen over pipe for complex compositions with multiple steps.
Type Your Errors: Always use tagged unions for error types to enable catchTag and better error handling.
Distinguish Errors from Defects: Use Effect.try/tryPromise for operations that can fail. Let unexpected errors become defects.
Keep Effects Pure: Don't perform side effects outside of Effect constructors. Use Effect.sync for side effects.
Use Descriptive Names: Name effects based on what they do, not how they do it (e.g., fetchUser not makeHttpRequest ).
Compose Small Effects: Build complex operations from small, focused effects that do one thing well.
Handle Requirements Explicitly: Use Effect.service and layers to manage dependencies rather than importing directly.
Document Effect Types: Explicitly type effects to make requirements, errors, and success types clear.
Use pipe for Transformations: For simple transformations, pipe is more concise than Effect.gen.
Test Effects Independently: Design effects to be testable by injecting dependencies via requirements.
Common Pitfalls
Using runSync on Async Effects: runSync throws on async effects. Use runPromise instead.
Not Handling Errors: Forgetting that effects can fail. Always consider the error channel.
Mixing Promises and Effects: Converting between promises and effects incorrectly. Use Effect.promise/tryPromise.
Ignoring Requirements: Not providing required services causes runtime errors. Use layers properly.
Throwing in Effect.sync: Thrown errors become defects. Use Effect.try for operations that can throw.
Not Using Effect.gen: Complex pipe chains are hard to read. Use Effect.gen for better readability.
Incorrect Error Types: Using unknown or Error instead of specific tagged error types.
Sequential When Parallel Is Better: Using Effect.gen sequentially when operations could run in parallel with Effect.all.
Over-Using map/flatMap: Effect.gen is clearer for multi-step operations than nested maps.
Not Leveraging Type Safety: Not using TypeScript's type system to catch errors at compile time.
When to Use This Skill
Use effect-core-patterns when you need to:
-
Build type-safe applications with Effect
-
Create and compose effectful operations
-
Handle errors in a type-safe manner
-
Work with async operations and promises
-
Manage side effects explicitly
-
Create pipelines of transformations
-
Convert callback-based APIs to Effect
-
Build maintainable, composable code
-
Leverage functional programming patterns
-
Ensure compile-time safety for effects
Resources
Official Documentation
-
Effect Website
-
Getting Started
-
The Effect Type
-
Creating Effects
-
Using Generators
-
Running Effects
Guides
-
Effect GitHub
-
Effect Discord
-
Effect Examples