Skip to content

Creating Effects

Most real programs need to bring the outside world into Effect: a value you already have, some synchronous code, a Promise-based client, a possibly-null lookup, or a callback API. Effect provides a small, precise set of constructors for each case. Picking the right one is mostly about one question: can this fail, and if so, how should the failure be typed?

import { Effect, Schema } from "effect"
class InvalidPayload extends Schema.TaggedErrorClass<InvalidPayload>()(
"InvalidPayload",
{ input: Schema.String, cause: Schema.Defect }
) {}
class UserLookupError extends Schema.TaggedErrorClass<UserLookupError>()(
"UserLookupError",
{ userId: Schema.Number, cause: Schema.Defect }
) {}
class MissingWorkspaceId extends Schema.TaggedErrorClass<MissingWorkspaceId>()(
"MissingWorkspaceId",
{}
) {}
const requestHeaders = new Map<string, string>([["x-request-id", "req_1"]])
const users = new Map([
[1, { id: 1, name: "Ada" }],
[2, { id: 2, name: "Lin" }]
])
// `Effect.succeed` wraps a value you already have in memory.
export const fromValue = Effect.succeed({ env: "prod", retries: 3 })
// `Effect.sync` wraps a synchronous side effect that will NOT throw.
export const fromSyncSideEffect = Effect.sync(() => Math.random())
// `Effect.try` wraps synchronous code that MAY throw, mapping the thrown
// value into a typed error.
export const parsePayload = Effect.fn("parsePayload")((input: string) =>
Effect.try({
try: () => JSON.parse(input) as { readonly userId: number },
catch: (cause) => new InvalidPayload({ input, cause })
})
)
// `Effect.tryPromise` wraps a Promise-based API that may reject or throw.
export 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 })
})
)
// `Effect.fromNullishOr` turns a nullable value into a typed effect, then we
// map the built-in NoSuchElementError to a domain error.
export const fromNullishHeader = Effect.fromNullishOr(
requestHeaders.get("x-workspace-id")
).pipe(Effect.mapError(() => new MissingWorkspaceId()))
// `Effect.callback` wraps a callback-style asynchronous API.
export const fromCallback = Effect.callback<number>((resume) => {
const timeoutId = setTimeout(() => resume(Effect.succeed(200)), 10)
// Return a finalizer so interruption can cancel the source.
return Effect.sync(() => clearTimeout(timeoutId))
})

Use Effect.succeed(value) to lift a value you already have, and Effect.fail(error) for the failure channel. Both are eager about the value but lazy about running - the effect still does nothing until executed.

import { Effect } from "effect"
const ok = Effect.succeed(42) // Effect<number>
const bad = Effect.fail("nope" as const) // Effect<never, "nope">
  • Effect.sync(thunk) - for synchronous side effects that are guaranteed not to throw (reading a clock-free constant, generating a random number, pushing to an in-memory buffer). If the thunk does throw, the exception is treated as an unrecoverable defect, not a typed error.
  • Effect.try({ try, catch }) - for synchronous code that can throw (like JSON.parse). The catch function maps the thrown value into a typed error in the error channel.
import { Effect } from "effect"
// Will not throw -> sync
const random = Effect.sync(() => Math.random())
// Might throw -> try, with the error mapped into the type
const parse = (input: string) =>
Effect.try({
try: () => JSON.parse(input) as unknown,
catch: (cause) => new Error(`Invalid JSON: ${String(cause)}`)
})
  • Effect.promise(thunk) - when the Promise is guaranteed not to reject. A rejection becomes a defect.
  • Effect.tryPromise({ try, catch }) - when rejection is expected. The catch function turns the rejection into a typed error.

The thunk receives an AbortSignal, so a well-behaved client can be cancelled when the effect is interrupted:

import { Effect } from "effect"
const getJson = (url: string) =>
Effect.tryPromise({
// The signal is wired to Effect interruption.
try: (signal) => fetch(url, { signal }).then((res) => res.json()),
catch: (cause) => new Error(`Request failed: ${String(cause)}`)
})

Notice there is no async/await in your Effect code - the Promise lives entirely inside the try thunk, and Effect takes it from there.

Effect.fromNullishOr(value) succeeds with the value when it is non-null and fails with a built-in NoSuchElementError when it is null or undefined. Map that into a domain error when you want a meaningful failure:

import { Effect } from "effect"
const env = new Map<string, string>([["PORT", "8080"]])
const requireEnv = (key: string) =>
Effect.fromNullishOr(env.get(key)).pipe(
Effect.mapError(() => new Error(`Missing env var: ${key}`))
)

Effect.callback adapts a callback-style asynchronous API. You receive a resume function and call it with an effect (typically Effect.succeed or Effect.fail) when the operation completes. Returning a finalizer effect lets Effect cancel the underlying source if the fiber is interrupted before it resolves.

import { Effect } from "effect"
const delay = (ms: number) =>
Effect.callback<void>((resume) => {
const id = setTimeout(() => resume(Effect.succeed(undefined)), ms)
// Cleanup on interruption.
return Effect.sync(() => clearTimeout(id))
})
You have…UseFailure channel
A value in memoryEffect.succeednone
An error valueEffect.failthe error you pass
Sync code that won’t throwEffect.syncdefect if it throws
Sync code that may throwEffect.trymapped by catch
A non-rejecting PromiseEffect.promisedefect if it rejects
A Promise that may rejectEffect.tryPromisemapped by catch
A possibly-null valueEffect.fromNullishOrNoSuchElementError
A callback-based async APIEffect.callbackwhatever you resume with

Once you have an effect, compose it with pipelines and handle the failures with Error Management.