The Effect type is the foundation of Effect programming. It represents a computation that can succeed, fail, or require dependencies from the environment.
What is an Effect?
An Effect<A, E, R> is a description of a computation that:
- Produces a value of type
A on success
- Can fail with an error of type
E
- Requires dependencies of type
R from the environment
Effects are immutable values that describe computations. They don’t execute until you run them with a runtime.
Creating Effects
Effect provides several ways to create effects from common sources:
From Plain Values
Use Effect.succeed to wrap values you already have in memory:
import { Effect } from "effect"
const config = Effect.succeed({ env: "prod", retries: 3 })
From Synchronous Code
Use Effect.sync for synchronous side effects that should not throw:
const timestamp = Effect.sync(() => Date.now())
Use Effect.try for synchronous code that may throw:
import { Effect, Schema } from "effect"
class InvalidPayload extends Schema.TaggedErrorClass<InvalidPayload>()("InvalidPayload", {
input: Schema.String,
cause: Schema.Defect
}) {}
const parsePayload = Effect.fn("parsePayload")((input: string) =>
Effect.try({
try: () => JSON.parse(input) as { readonly userId: number },
catch: (cause) => new InvalidPayload({ input, cause })
})
)
From Promises
Use Effect.tryPromise to wrap Promise-based APIs:
import { Effect, Schema } from "effect"
class UserLookupError extends Schema.TaggedErrorClass<UserLookupError>()("UserLookupError", {
userId: Schema.Number,
cause: Schema.Defect
}) {}
const users = new Map<number, { readonly id: number; readonly name: string }>([
[1, { id: 1, name: "Ada" }],
[2, { id: 2, name: "Lin" }]
])
const fetchUser = Effect.fn("fetchUser")((userId: number) =>
Effect.tryPromise({
async try() {
const user = users.get(userId)
if (!user) {
throw new Error(`Missing user ${userId}`)
}
return user
},
catch: (cause) => new UserLookupError({ userId, cause })
})
)
From Nullable Values
Use Effect.fromNullishOr to turn nullable values into typed effects:
import { Effect, Schema } from "effect"
class MissingWorkspaceId extends Schema.TaggedErrorClass<MissingWorkspaceId>()("MissingWorkspaceId", {}) {}
const requestHeaders = new Map<string, string>([
["x-request-id", "req_1"]
])
const workspaceId = Effect.fromNullishOr(requestHeaders.get("x-workspace-id")).pipe(
Effect.mapError(() => new MissingWorkspaceId())
)
From Callbacks
Use Effect.callback to wrap callback-style APIs:
const delayed = Effect.callback<number>((resume) => {
const timeoutId = setTimeout(() => {
resume(Effect.succeed(200))
}, 10)
// Return a finalizer so interruption can cancel the callback
return Effect.sync(() => {
clearTimeout(timeoutId)
})
})
Writing Effect Code
Effect provides two primary ways to write effectful computations: Effect.gen and Effect.fn.
Using Effect.gen
Use Effect.gen to write code in an imperative style similar to async/await. You can use yield* to access the result of an effect:
import { Effect, Schema } from "effect"
class FileProcessingError extends Schema.TaggedErrorClass<FileProcessingError>()("FileProcessingError", {
message: Schema.String
}) {}
const processFile = Effect.gen(function*() {
yield* Effect.log("Starting the file processing...")
yield* Effect.log("Reading file...")
// Always return when raising an error, to ensure TypeScript understands that
// the function will not continue executing.
return yield* new FileProcessingError({ message: "Failed to read the file" })
}).pipe(
// Add additional functionality with .pipe
Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)),
Effect.withSpan("fileProcessing", {
attributes: {
method: "Effect.gen"
}
})
)
Always use return yield* when raising errors in Effect.gen to ensure TypeScript understands the control flow.
Using Effect.fn
When writing functions that return an Effect, use Effect.fn instead of creating functions that return Effect.gen:
import { Effect, Schema } from "effect"
class SomeError extends Schema.TaggedErrorClass<SomeError>()("SomeError", {
message: Schema.String
}) {}
// Pass a string to Effect.fn, which will improve stack traces and also
// attach a tracing span (using Effect.withSpan behind the scenes).
//
// The name string should match the function name.
export const effectFunction = Effect.fn("effectFunction")(
// You can use `Effect.fn.Return` to specify the return type of the function.
// It accepts the same type parameters as `Effect.Effect`.
function*(n: number): Effect.fn.Return<string, SomeError> {
yield* Effect.logInfo("Received number:", n)
// Always return when raising an error
return yield* new SomeError({ message: "Failed to read the file" })
},
// Add additional functionality by passing in additional arguments.
// **Do not** use .pipe with Effect.fn
Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)),
Effect.annotateLogs({
method: "effectFunction"
})
)
Do not use .pipe with Effect.fn. Instead, pass additional combinators as extra arguments to Effect.fn.
Common Operations
Use .pipe() to chain operations on effects:
const result = Effect.succeed(5).pipe(
Effect.map(n => n * 2),
Effect.tap(n => Effect.log(`Result: ${n}`))
)
Sequencing Effects
Effects can be sequenced using Effect.gen or combinators:
const program = Effect.gen(function*() {
const user = yield* fetchUser(1)
const profile = yield* fetchProfile(user.id)
return profile
})
Handling Success and Failure
Use pattern matching or combinators to handle both channels:
const handled = effect.pipe(
Effect.match({
onFailure: (error) => `Error: ${error}`,
onSuccess: (value) => `Success: ${value}`
})
)
Next Steps
Learn about Services
Discover how to structure your application with Services