| name | effect-core-patterns |
| user-invocable | false |
| description | Use when Effect core patterns including Effect<A, E, R> type, succeed, fail, sync, promise, and Effect.gen for composing effects. Use for basic Effect operations. |
| allowed-tools | ["Bash","Read","Write","Edit"] |
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"
const simpleEffect: Effect.Effect<number, never, never> = Effect.succeed(42)
const failableEffect: Effect.Effect<number, string, never> =
Effect.fail("Something went wrong")
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)
const user = Effect.succeed({ id: "1", name: "Alice" })
const voidEffect = Effect.succeed(undefined)
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)
}
const program = Effect.gen(function* () {
const age = yield* validateAge(-5)
return age
})
Effect.sync - Synchronous Side Effects
Use for synchronous operations with side effects:
import { Effect } from "effect"
let counter = 0
const incrementCounter = Effect.sync(() => {
counter++
return counter
})
const log = (message: string) =>
Effect.sync(() => {
console.log(message)
})
const now = Effect.sync(() => Date.now())
Effect.try - Synchronous Operations That May Fail
Use for sync operations that might throw:
import { Effect } from "effect"
const parseJSON = (text: string): Effect.Effect<unknown, Error, never> =>
Effect.try(() => JSON.parse(text))
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)
})
})
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"
const delay = (ms: number): Effect.Effect<void, never, never> =>
Effect.promise(() =>
new Promise<void>((resolve) => setTimeout(resolve, ms))
)
const fetchData = (url: string): Effect.Effect<Response, never, never> =>
Effect.promise(() => fetch(url))
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
})
})
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"
const sleep = (ms: number): Effect.Effect<void, never, never> =>
Effect.async<void>((resume) => {
const timeoutId = setTimeout(() => {
resume(Effect.succeed(undefined))
}, ms)
return Effect.sync(() => {
clearTimeout(timeoutId)
})
})
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"
const program = Effect.gen(function* () {
const a = yield* Effect.succeed(10)
const b = yield* Effect.succeed(20)
return a + b
})
const programWithErrors = Effect.gen(function* () {
const age = yield* validateAge(25)
const user = yield* createUser({ age })
return user
})
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 }
})
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 }
}
})
const safeDivide = (a: number, b: number) =>
Effect.gen(function* () {
if (b === 0) {
yield* Effect.fail({ _tag: "DivideByZero" })
return
}
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))
try {
Effect.runSync(Effect.fail("error"))
} catch (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)
try {
await Effect.runPromise(Effect.fail("error"))
} catch (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
const result = pipe(
Effect.succeed(21),
Effect.map(double)
)
const result2 = Effect.succeed(21).pipe(
Effect.map(double)
)
const program = pipe(
Effect.succeed("hello"),
Effect.map(s => s.toUpperCase()),
Effect.map(s => s.length)
)
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> => {
}
const program = pipe(
getUser("123"),
Effect.flatMap(user => getUserPosts(user.id))
)
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"
const program = pipe(
log("Starting..."),
Effect.andThen(processData()),
Effect.andThen(log("Done!"))
)
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)
)
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())
)
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
Guides