Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
↑↓navigate↵open⌘Jpreview
On this page

Effect Integration

Write effectful oRPC handlers with Effect generators, call oRPC clients as effects, provide services through context, and use Effect Schema for input and output validation.

Installation

npm install @orpc/experimental-effect@beta effect
pnpm add @orpc/experimental-effect@beta effect
yarn add @orpc/experimental-effect@beta effect
bun add @orpc/experimental-effect@beta effect
nub add @orpc/experimental-effect@beta effect
aube add @orpc/experimental-effect@beta effect

Effectful Handlers

handlerGen allows you to write effectful handlers using generator functions. Inside the generator, you can yield Effect operations, and handlerGen will handle the execution and error handling for you.

import { function handlerGen<TCurrentContext extends Context, TInput, TErrorConstructorMap extends ORPCErrorConstructorMap<any>, TYield extends Effect.Effect<any, any, InferEffectServices<TCurrentContext>>, TReturn>(handler: HandlerGen<TCurrentContext, TInput, TYield, TReturn, TErrorConstructorMap>): ProcedureHandler<TCurrentContext, TInput, TReturn, TErrorConstructorMap>
Creates a procedure handler from an Effect generator function. Inside the generator you can yield Effect operations, and `handlerGen` handles the execution and error handling for you.
@see{@link https://orpc.dev/docs/integrations/effect#effectful-handlers Effect Integration - Effectful Handlers}
handlerGen
} from '@orpc/experimental-effect'
import { import EffectEffect } from 'effect' const const procedure: DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>procedure = const os: Builder<DefaultInitialContext & object, Record<never, never>>
The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler` to define procedures, then compose them into routers.
@see{@link https://orpc.dev/docs/procedure Procedure}
os
.Builder<DefaultInitialContext & object, Record<never, never>>.handler<number>(handler: ProcedureHandler<DefaultInitialContext & object, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<DefaultInitialContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>
Defines the function that implements the procedure and completes the chain, returning a callable procedure.
@see{@link https://orpc.dev/docs/procedure Procedure}
handler
(handlerGen<DefaultInitialContext & object, unknown, ORPCErrorConstructorMap<Record<never, never>>, Effect.Effect<number, never, never>, number>(handler: HandlerGen<DefaultInitialContext & object, unknown, Effect.Effect<number, never, never>, number, ORPCErrorConstructorMap<Record<never, never>>>): ProcedureHandler<DefaultInitialContext & object, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>
Creates a procedure handler from an Effect generator function. Inside the generator you can yield Effect operations, and `handlerGen` handles the execution and error handling for you.
@see{@link https://orpc.dev/docs/integrations/effect#effectful-handlers Effect Integration - Effectful Handlers}
handlerGen
(function* ({ input: unknowninput, context: DefaultInitialContext & objectcontext }) {
// You can use Effect's features here, such as concurrency, error handling, etc. const const result: numberresult = yield* import EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>
Creates an `Effect` that represents an asynchronous computation guaranteed to succeed. **When to use** Use to convert a `Promise` into an `Effect` when the async operation is guaranteed to succeed and will not reject. **Details** An optional `AbortSignal` can be provided to allow for interruption of the wrapped `Promise` API. **Gotchas** The `Promise` must not reject. If it rejects, the rejection is treated as a defect, not as a typed failure. Use `tryPromise` when rejection is expected. Interruption aborts the provided `AbortSignal`, but the underlying asynchronous operation only stops if it observes that signal. **Example** (Wrapping a non-rejecting Promise) ```ts import.meta.vitest import { Effect } from "effect" const succeedAsync = (message: string) => Effect.promise<string>(() => Promise.resolve(message)) // ┌─── Effect<string, never, never> // ▼ const program = succeedAsync("Async operation completed successfully!") await Effect.runPromise(program) // => "Async operation completed successfully!" ```
@see{@link tryPromise} for a version that can handle failures.@stabilitystable@categoryconstructors@since2.0.0
promise
(() => var Promise: PromiseConstructor
Represents the completion of an asynchronous operation
Promise
.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)
Creates a new resolved promise for the provided value.
@paramvalue A promise.@returnsA promise whose internal state matches the provided promise.
resolve
(5))
return const result: numberresult }))

.effect extension

Import @orpc/experimental-effect/extensions/effect from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds an .effect method to the builder so you can write effectful handlers directly.

const procedure = base.effect(function* ({ input, context }) {
  // You can use Effect's features here, such as concurrency, error handling, etc.
  const result = yield* Effect.promise(() => Promise.resolve(5))
  return result
})
import '@orpc/experimental-effect/extensions/effect'

import { os } from '@orpc/server'

export const base = os

.effect also works when implementing a contract:

const findPlanet = implement(contract).planet.find.effect(function* ({ input }) {
  return yield* Effect.promise(() => db.planet.find(input.id))
})

Effect Services

You can provide Effect services through the oRPC context in a typesafe way with WithEffectContext and effect/context:

import { function handlerGen<TCurrentContext extends Context, TInput, TErrorConstructorMap extends ORPCErrorConstructorMap<any>, TYield extends Effect.Effect<any, any, InferEffectServices<TCurrentContext>>, TReturn>(handler: HandlerGen<TCurrentContext, TInput, TYield, TReturn, TErrorConstructorMap>): ProcedureHandler<TCurrentContext, TInput, TReturn, TErrorConstructorMap>
Creates a procedure handler from an Effect generator function. Inside the generator you can yield Effect operations, and `handlerGen` handles the execution and error handling for you.
@see{@link https://orpc.dev/docs/integrations/effect#effectful-handlers Effect Integration - Effectful Handlers}
handlerGen
, interface WithEffectContext<Services>
A context shape that provides Effect services to effectful handlers through the oRPC context in a typesafe way.
@see{@link https://orpc.dev/docs/integrations/effect#effect-services Effect Integration - Effect Services}
WithEffectContext
} from '@orpc/experimental-effect'
import { import ContextContext, import EffectEffect } from 'effect' class class RandomRandom extends import ContextContext.
const Service: <Random, {
    readonly next: Effect.Effect<number>;
}>() => <Identifier, E, R, Args>(id: Identifier, options?: {
    readonly make?: ((...args: Args) => Effect.Effect<{
        readonly next: Effect.Effect<number>;
    }, E, R>) | Effect.Effect<{
        readonly next: Effect.Effect<number>;
    }, E, R> | undefined;
} | undefined) => Context.ServiceClass<Random, Identifier, {
    readonly next: Effect.Effect<number>;
}> & ([unassigned] extends [R] ? unknown : {
    ...;
}) (+2 overloads)
Creates a `Context` service key. **When to use** Use when you need to define a context service key for a dependency that must be provided by the surrounding context. **Details** Call `Context.Service("Key")` for a function-style key, or use the two-stage form `Context.Service<Self, Shape>()("Key")` for class-style service declarations. The returned key can be yielded as an Effect and passed to `Context.make`, `Context.add`, and the Context getter functions. **Gotchas** The string key is the runtime identity of the service. Reusing the same key string for unrelated services makes them occupy the same slot in a `Context`. **Example** (Creating service keys) ```ts import.meta.vitest import { Context } from "effect" // Create a simple service const Database = Context.Service<{ query: (sql: string) => string }>("Database") // Create a service class class Config extends Context.Service<Config, { port: number }>()("Config") {} // Use the services to create contexts const db = Context.make(Database, { query: (sql) => `Result: ${sql}` }) const config = Context.make(Config, { port: 8080 }) Context.get(db, Database).query("SELECT 1") // => "Result: SELECT 1" Context.get(config, Config).port // => 8080 ```
@see{@link Reference} for service keys with default values@stabilitystable@categoryservices@since4.0.0
Service
<
class RandomRandom, { readonly next: Effect.Effect<number, never, never>next: import EffectEffect.interface Effect<out A, out E = never, out R = never>
The `Effect` interface defines a value that lazily describes a workflow or job. The workflow requires some context `R`, and may fail with an error of type `E`, or succeed with a value of type `A`. **When to use** Use when you need to represent a lazy, composable workflow that can require services, fail with a typed error, or succeed with a typed value. **Details** `Effect` values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability. To run an `Effect` value, you need a `Runtime`, which is a type that is capable of executing `Effect` values.
@stabilitystable@categorymodels@since2.0.0
Effect
<number>
} >()('app/Random') {} interface ServerContext extends interface WithEffectContext<Services>
A context shape that provides Effect services to effectful handlers through the oRPC context in a typesafe way.
@see{@link https://orpc.dev/docs/integrations/effect#effect-services Effect Integration - Effect Services}
WithEffectContext
<class RandomRandom> {}
const const procedure: DecoratedProcedure<ServerContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>procedure = const os: Builder<DefaultInitialContext & object, Record<never, never>>
The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler` to define procedures, then compose them into routers.
@see{@link https://orpc.dev/docs/procedure Procedure}
os
.Builder<DefaultInitialContext & object, Record<never, never>>.$context<ServerContext>(): Builder<ServerContext & object, Record<never, never>>
Declares the initial context type that must be provided when executing procedures built from this builder.
@see{@link https://orpc.dev/docs/context#initial-context Context - Initial Context}
$context
<ServerContext>()
.Builder<ServerContext & object, Record<never, never>>.handler<number>(handler: ProcedureHandler<ServerContext & object, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<ServerContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>
Defines the function that implements the procedure and completes the chain, returning a callable procedure.
@see{@link https://orpc.dev/docs/procedure Procedure}
handler
(
handlerGen<ServerContext & object, unknown, ORPCErrorConstructorMap<Record<never, never>>, Effect.Effect<number, never, never> | Effect.Effect<{
    readonly next: Effect.Effect<number>;
}, never, Random>, number>(handler: HandlerGen<ServerContext & object, unknown, Effect.Effect<number, never, never> | Effect.Effect<{
    readonly next: Effect.Effect<number>;
}, never, Random>, number, ORPCErrorConstructorMap<Record<never, never>>>): ProcedureHandler<...>
Creates a procedure handler from an Effect generator function. Inside the generator you can yield Effect operations, and `handlerGen` handles the execution and error handling for you.
@see{@link https://orpc.dev/docs/integrations/effect#effectful-handlers Effect Integration - Effectful Handlers}
handlerGen
(function* ({ input: unknowninput, context: ServerContext & objectcontext }) {
const
const random: {
    readonly next: Effect.Effect<number>;
}
random
= yield* class RandomRandom
const const result: numberresult = yield*
const random: {
    readonly next: Effect.Effect<number>;
}
random
.next: Effect.Effect<number, never, never>next
return const result: numberresult })) const const random: numberrandom = await
call<{
    'effect/context': Context.Context<Random>;
}, InitialInputSchema, Schema<number>, Record<never, never>>(lazyableProcedure: Lazyable<Procedure<{
    'effect/context': Context.Context<Random>;
}, any, InitialInputSchema, Schema<number>, Record<never, never>>>, input: void, options: CallOptions<{
    'effect/context': Context.Context<Random>;
}, Schema<number>, Record<never, never>>): PromiseWithError<...>
Quickly call a procedure without creating a client.
@example```ts const output = await call(getting, 'input') const output = await call(getting, 'input', { context: { db: 'postgres' } }) ```@see{@link https://orpc.dev/docs/client/server-side#one-off-calls Server-Side Clients - One-Off Calls}
call
(const procedure: DecoratedProcedure<ServerContext & object, object, InitialInputSchema, Schema<number>, Record<never, never>>procedure, var undefinedundefined, {
context: Value<Promisable<{
    'effect/context': Context.Context<Random>;
}>, [clientContext: object]>
context
: {
'effect/context': import ContextContext.const empty: () => Context.Context<never>
Returns an empty `Context`. **Example** (Creating an empty context) ```ts import.meta.vitest import { Context } from "effect" Context.empty().mapUnsafe.size // => 0 ```
@stabilitystable@categoryconstructors@since2.0.0
empty
().Pipeable.pipe<Context.Context<never>, Context.Context<Random>>(this: Context.Context<never>, ab: (_: Context.Context<never>) => Context.Context<Random>): Context.Context<Random> (+21 overloads)pipe(
import ContextContext.
const add: <Random, {
    readonly next: Effect.Effect<number>;
}>(key: Context.Key<Random, {
    readonly next: Effect.Effect<number>;
}>, service: {
    readonly next: Effect.Effect<number>;
}) => <Services>(self: Context.Context<Services>) => Context.Context<Random | Services> (+1 overload)
Adds a service to a given `Context`. **When to use** Use when you need to store a known service value in a `Context`. **Details** If the context already contains the same service key, the new service replaces the previous one. **Example** (Adding a service to a context) ```ts import.meta.vitest import { Context, pipe } from "effect" const Port = Context.Service<{ PORT: number }>("Port") const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") const someContext = Context.make(Port, { PORT: 8080 }) const context = pipe( someContext, Context.add(Timeout, { TIMEOUT: 5000 }) ) const values = [Context.get(context, Port).PORT, Context.get(context, Timeout).TIMEOUT] values // => [8080, 5000] ```
@see{@link addOrOmit} for adding or removing a service from an `Option`@stabilitystable@categorycombining@since2.0.0
add
(class RandomRandom, {
next: Effect.Effect<number, never, never>next: import EffectEffect.const sync: <number>(thunk: LazyArg<number>) => Effect.Effect<number, never, never>
Creates an `Effect` that represents a synchronous side-effectful computation. **When to use** Use when you need to wrap a synchronous side-effectful operation that is not expected to throw. **Details** The provided function is evaluated lazily when the effect runs. **Gotchas** The function must not throw. If it throws, the thrown value is treated as a defect, not as a typed failure. Use `try` when throwing is expected. **Example** (Capturing synchronous logging in an Effect) ```ts import.meta.vitest import { Effect } from "effect" const output: Array<unknown> = [] const log = (message: string) => Effect.sync(() => { void output.push(message) // side effect }) // ┌─── Effect<void, never, never> // ▼ const program = log("Hello, World!") Effect.runSync(program) output // => ["Hello, World!"] ```
@see{@link try_try} for a version that can handle failures.@stabilitystable@categoryconstructors@since2.0.0
sync
(() => var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.random(): number
Returns a pseudorandom number between 0 and 1.
random
()),
}), ) } })

Error Handling

This integration preserves the original error whenever possible. If you call Effect.fail(error), the error is forwarded to middleware and interceptors, just like a regular thrown error.

To customize this behavior, wrap the effect before execution using effect/wrap in the context:

import { class ORPCError<TCode extends ORPCErrorCode, TData>
Typed error carrying a `code`, a `message`, and optional `data`. Throw it from handlers or middleware to produce typed error responses on the client.
@see{@link https://orpc.dev/docs/error-handling#orpcerror-class Error Handling - ORPCError Class}
ORPCError
} from '@orpc/server'
import { import CauseCause, import ContextContext, import DataData, import EffectEffect } from 'effect' class class NotFoundErrorNotFoundError extends import DataData.
const TaggedError: <"NotFoundError">(tag: "NotFoundError") => new <A>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
    readonly _tag: "NotFoundError";
} & Readonly<A>
Creates a tagged error class with a `_tag` discriminator. **When to use** Use when you need domain errors with discriminated-union handling. **Details** Like {@link Error } , but instances also carry a `readonly _tag` property, enabling `Effect.catchTag` and `Effect.catchTags` for tag-based recovery. The `_tag` is excluded from the constructor argument. Yielding an instance inside `Effect.gen` fails the effect with this error. **Example** (Recovering by tag) ```ts import.meta.vitest import { Data, Effect } from "effect" class NotFound extends Data.TaggedError("NotFound")<{ readonly resource: string }> {} class Forbidden extends Data.TaggedError("Forbidden")<{ readonly reason: string }> {} const program = Effect.gen(function*() { return yield* new NotFound({ resource: "/users/42" }) }) const recovered = program.pipe( Effect.catchTag("NotFound", (e) => Effect.succeed(`missing: ${e.resource}`)) ) await Effect.runPromise(recovered) // => "missing: /users/42" ```
@see{@link Error} — without a `_tag`@see{@link TaggedClass} — tagged class that is not an error@stabilitystable@categoryconstructors@since2.0.0
TaggedError
('NotFoundError')<{
readonly resource: stringresource: string }> {} export async function function fetch(request: Request): Promise<Response>fetch(request: Requestrequest: Request) { const { const matched: booleanmatched, const response: Response | undefinedresponse } = await const handler: RPCHandler<ServerContext>handler.FetchHandler<ServerContext>.handle(request: Request, options: FriendlyStandardHandlerHandleOptions<ServerContext>): Promise<FetchHandlerHandleResult>handle(request: Requestrequest, { context: Value<Promisable<ServerContext>, [request: StandardLazyRequest]>context: { 'effect/context': import ContextContext.const empty: () => Context.Context<never>
Returns an empty `Context`. **Example** (Creating an empty context) ```ts import.meta.vitest import { Context } from "effect" Context.empty().mapUnsafe.size // => 0 ```
@stabilitystable@categoryconstructors@since2.0.0
empty
(),
'effect/wrap': effect: Effect.Effect<A, unknown, never>effect => effect: Effect.Effect<A, unknown, never>effect.Pipeable.pipe<Effect.Effect<A, unknown, never>, Effect.Effect<A, unknown, never>>(this: Effect.Effect<A, unknown, never>, ab: (_: Effect.Effect<A, unknown, never>) => Effect.Effect<A, unknown, never>): Effect.Effect<A, unknown, never> (+21 overloads)pipe( import EffectEffect.const catchCause: <unknown, never, unknown, never>(f: (cause: Cause.Cause<unknown>) => Effect.Effect<never, unknown, never>) => <A, R>(self: Effect.Effect<A, unknown, R>) => Effect.Effect<A, unknown, R> (+1 overload)
Handles both recoverable and unrecoverable errors by providing a recovery effect. **When to use** Use when you need to recover from an `Effect` by inspecting the full `Cause`, including recoverable failures, defects, and interruptions, instead of only the typed error value. **Details** When to Recover from Defects: Defects are unexpected errors that typically shouldn't be recovered from, as they often indicate serious issues. However, in some cases, such as dynamically loaded plugins, controlled recovery might be needed. **Example** (Recovering from full failure causes) ```ts import.meta.vitest import { Cause, Effect } from "effect" const output: Array<unknown> = [] // An effect that might fail in different ways const program = Effect.die("Something went wrong") // Recover from any cause (including defects) const recovered = Effect.catchCause(program, (cause) => { if (Cause.hasDies(cause)) { return Effect.sync(() => { output.push("Caught defect") }).pipe( Effect.as("Recovered from defect") ) } return Effect.succeed("Unknown error") }) void output.push(Effect.runSync(recovered)) output // => ["Caught defect", "Recovered from defect"] ```
@stabilitystable@categoryerror handling@since4.0.0
catchCause
((cause: Cause.Cause<unknown>cause) => {
const const error: unknownerror = import CauseCause.const squash: <unknown>(self: Cause.Cause<unknown>) => unknown
Collapses a `Cause` into a single `unknown` value, picking the "most important" failure in this order: **When to use** Use to collapse a structured cause to the single value that synchronous and promise runners would throw. **Details** 1. First `Fail` error (the `E` value) 2. First `Die` defect 3. A generic `Error("All fibers interrupted without error")` for interrupt-only causes 4. A generic `Error("Empty cause")` for `empty` This is the function used by `Effect.runPromise` and `Effect.runSync` to decide what to throw. **Gotchas** This function is lossy. Use {@link prettyErrors } or iterate `cause.reasons` when you need all failures. **Example** (Squashing a cause) ```ts import.meta.vitest import { Cause } from "effect" Cause.squash(Cause.fail("error")) // => "error" Cause.squash(Cause.die("defect")) // => "defect" ```
@see{@link prettyErrors} — non-lossy conversion to `Array<Error>`@see{@link pretty} — human-readable string rendering@stabilitystable@categorydestructors@since2.0.0
squash
(cause: Cause.Cause<unknown>cause)
if (const error: unknownerror instanceof class NotFoundErrorNotFoundError) { return import EffectEffect.const fail: <ORPCError<"NOT_FOUND", unknown>>(error: ORPCError<"NOT_FOUND", unknown>) => Effect.Effect<never, ORPCError<"NOT_FOUND", unknown>, never>
Creates an `Effect` that represents a recoverable error. **When to use** Use to explicitly signal a recoverable error in an `Effect`. **Details** The error keeps propagating unless it is handled. You can handle tagged errors with functions like {@link catchTag } or {@link catchTags } . **Example** (Creating a failed effect) ```ts import.meta.vitest import { Data, Effect } from "effect" class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {} // ┌─── Effect<never, OperationFailedError, never> // ▼ const failure = Effect.fail( new OperationFailedError() ) Effect.runSync(Effect.flip(failure))._tag // => "OperationFailedError" ```
@see{@link succeed} to create an effect that represents a successful value.@stabilitystable@categoryconstructors@since2.0.0
fail
(new new ORPCError<"NOT_FOUND", unknown>(code: "NOT_FOUND", options?: ORPCErrorOptions<unknown> | undefined): ORPCError<"NOT_FOUND", unknown>
Typed error carrying a `code`, a `message`, and optional `data`. Throw it from handlers or middleware to produce typed error responses on the client.
@see{@link https://orpc.dev/docs/error-handling#orpcerror-class Error Handling - ORPCError Class}
ORPCError
('NOT_FOUND', {
message?: string | undefinedmessage: `${const error: NotFoundErrorerror.resource: stringresource} not found`, ErrorOptions.cause?: unknowncause: const error: NotFoundErrorerror, })) } return import EffectEffect.const failCause: <unknown>(cause: Cause.Cause<unknown>) => Effect.Effect<never, unknown, never>
Creates an `Effect` that represents a failure with a specific `Cause`. **When to use** Use when you already have a full `Cause` and need to preserve defects, interruptions, annotations, or combined failures in the effect's failure channel. **Details** This function allows you to create effects that fail with complex error structures, including multiple errors, defects, interruptions, and more. **Example** (Failing with a full Cause) ```ts import.meta.vitest import { Cause, Effect } from "effect" const program = Effect.failCause( Cause.fail("Network error") ) Effect.runSync(Effect.flip(program)) // => "Network error" ```
@stabilitystable@categoryconstructors@since2.0.0
failCause
(cause: Cause.Cause<unknown>cause)
}), ), }, }) if (const matched: booleanmatched) { return const response: Responseresponse } return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response
The **`Response`** interface of the Fetch API represents the response to a request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)
Response
('Not Found', { ResponseInit.status?: number | undefinedstatus: 404 })
}

The wrap runs with effect/context, once per handlerGen and middlewareGen (see opts.kind). Keep it idempotent, since failures from inner layers pass through it again.

Typesafe Errors

An ORPCError that fails the effect, such as return yield* Effect.fail(new ORPCError(...)), is thrown from the handler exactly like a thrown ORPCError in a regular handler. Define your errors with .errors and fail with errors.X(...) to make them typesafe on the client:

const procedure = os
  .errors({
    NOT_FOUND: {
      message: 'The resource you are looking for does not exist',
    },
  })
  .handler(handlerGen(function* ({ errors }) {
    if (resourceNotFound) {
      return yield* Effect.fail(errors.NOT_FOUND())
    }

    return 'Success'
  }))

const [error, result] = await safe(call(procedure))

if (isDefinedError(error)) {
  // typesafe error handling
}

Catching ORPCErrors

Use catchORPCError to recover from every ORPCError failure in the error channel of an effect, or catchORPCErrorCode and catchORPCErrorCodes to recover from specific codes only. Recovered errors are excluded from the resulting effect, and other failures re-fail with their original cause:

import { catchORPCError, catchORPCErrorCode, catchORPCErrorCodes } from '@orpc/experimental-effect'
import { Effect } from 'effect'

const recovered = program.pipe(
  catchORPCError(error => Effect.succeed(`caught ${error.code}`)),
)

const fallback = program.pipe(
  catchORPCErrorCode('NOT_FOUND', error => Effect.succeed(null)),
)

const handled = program.pipe(
  catchORPCErrorCodes({
    NOT_FOUND: error => Effect.succeed(null),
    CONFLICT: error => Effect.succeed(error.message),
  }),
)

error.data is typed only when the error channel holds nothing but declared ORPCErrors. Otherwise, as with client calls, it is unknown.

Interruption

When a request is aborted, handlerGen and middlewareGen fibers are interrupted, so finalizers such as Effect.ensuring and Effect.onInterrupt run. Handlers have no Scope, so wrap Effect.acquireRelease in Effect.scoped.

Effectful Middleware

middlewareGen allows you to write effectful middleware using generator functions. Inside the generator, you can yield Effect operations and yield* next() to continue the chain. Downstream failures land in the error channel of next, so you can recover from them with Effect.

import { function middlewareGen<TInContext extends Context, TOutContext extends Context = object, TInput = unknown, TOutput = any, TErrorMap extends ErrorMap = Record<never, never>, TYield extends Effect<any, any, InferEffectServices<TInContext>> = Effect<any, any, InferEffectServices<TInContext>>>(middleware: MiddlewareGen<TInContext, TOutContext, TInput, TOutput, TYield, ORPCErrorConstructorMap<TErrorMap>>): Middleware<TInContext, TOutContext, TInput, TOutput, TErrorMap>
Creates an oRPC middleware from an Effect generator function. Inside the generator you can yield Effect operations and `yield* next()` to continue the chain, and `middlewareGen` handles the execution and error handling for you.
@see{@link https://orpc.dev/docs/integrations/effect#effectful-middleware Effect Integration - Effectful Middleware}
middlewareGen
} from '@orpc/experimental-effect'
import { import ClockClock } from 'effect' const
const procedure: DecoratedProcedure<{
    auth: boolean;
} & object, {
    startedAt: number;
}, InitialInputSchema, Schema<number>, Record<never, never>>
procedure
= const os: Builder<DefaultInitialContext & object, Record<never, never>>
The oRPC procedure builder. Chain methods like `.input`, `.use`, and `.handler` to define procedures, then compose them into routers.
@see{@link https://orpc.dev/docs/procedure Procedure}
os
.
Builder<DefaultInitialContext & object, Record<never, never>>.$context<{
    auth: boolean;
}>(): Builder<{
    auth: boolean;
} & object, Record<never, never>>
Declares the initial context type that must be provided when executing procedures built from this builder.
@see{@link https://orpc.dev/docs/context#initial-context Context - Initial Context}
$context
<{ auth: booleanauth: boolean }>()
.
Builder<{ auth: boolean; } & object, Record<never, never>>.use<{
    startedAt: number;
}, {
    auth: boolean;
} & object, Record<never, never>>(middleware: Middleware<{
    auth: boolean;
} & object, {
    startedAt: number;
}, unknown, unknown, Record<never, never>>): BuilderWithMiddlewares<{
    auth: boolean;
} & object, {
    startedAt: number;
}, Record<never, never>>
Applies a middleware that runs before the handler of every procedure built from this builder.
@see{@link https://orpc.dev/docs/middleware Middleware}
use
(
middlewareGen<{
    auth: boolean;
} & object, {
    startedAt: number;
}, unknown, unknown, Record<never, never>, Effect<number, never, never> | Effect<{
    output: unknown;
    context: {
        startedAt: number;
    };
}, unknown, never>>(middleware: MiddlewareGen<{
    auth: boolean;
} & object, {
    startedAt: number;
}, unknown, unknown, Effect<number, never, never> | Effect<{
    output: unknown;
    context: {
        startedAt: number;
    };
}, unknown, never>, ORPCErrorConstructorMap<Record<never, never>>>): Middleware<{
    auth: boolean;
} & object, {
    startedAt: number;
}, unknown, unknown, Record<...>>
Creates an oRPC middleware from an Effect generator function. Inside the generator you can yield Effect operations and `yield* next()` to continue the chain, and `middlewareGen` handles the execution and error handling for you.
@see{@link https://orpc.dev/docs/integrations/effect#effectful-middleware Effect Integration - Effectful Middleware}
middlewareGen
(function* ({
context: {
    auth: boolean;
} & object
context
, next: MiddlewareGenNext<unknown>next }) {
const const startedAt: numberstartedAt = yield* import ClockClock.const currentTimeMillis: Effect<number, never, never>
Returns an Effect that succeeds with the current Unix time in milliseconds. **When to use** Use to create wall-clock timestamps from the active `Clock` service with millisecond precision. **Gotchas** The value can move backward or forward when the system wall clock is corrected, so it is not suitable for measuring elapsed time. **Example** (Reading milliseconds) ```ts import.meta.vitest import { Clock, Effect } from "effect" const testClock: Clock.Clock = { currentTimeMillisUnsafe: () => 1_000, currentTimeMillis: Effect.succeed(1_000), monotonicTimeNanosUnsafe: () => 1_000_000_000n, monotonicTimeNanos: Effect.succeed(1_000_000_000n), currentTimeNanosUnsafe: () => 1_000_000_000n, currentTimeNanos: Effect.succeed(1_000_000_000n), sleep: () => Effect.void } await Effect.runPromise(Effect.provideService(Clock.currentTimeMillis, Clock.Clock, testClock)) // => 1_000 ```
@see{@link currentTimeNanos} for nanosecond precision@see{@link monotonicTimeNanos} for measuring elapsed time@see{@link clockWith} for accessing the full Clock service@stabilitystable@categoryaccessors@since2.0.0
currentTimeMillis
return yield*
next: MiddlewareGenNext
<{
    startedAt: number;
}>(options: {
    context: {
        startedAt: number;
    };
}) => Effect<{
    output: unknown;
    context: {
        startedAt: number;
    };
}, unknown, never>
next
({
context: {
    startedAt: number;
}
context
: { startedAt: numberstartedAt } })
})) .
BuilderWithMiddlewares<{ auth: boolean; } & object, { startedAt: number; }, Record<never, never>>['handler']<number>(handler: ProcedureHandler<DistributiveOmit<{
    auth: boolean;
} & object, "startedAt"> & {
    startedAt: number;
}, unknown, number, ORPCErrorConstructorMap<Record<never, never>>>): DecoratedProcedure<{
    auth: boolean;
} & object, {
    startedAt: number;
}, InitialInputSchema, Schema<number>, Record<never, never>>
Defines the function that implements the procedure and completes the chain, returning a callable procedure.
@see{@link https://orpc.dev/docs/procedure Procedure}
handler
(({
context: DistributiveOmit<{
    auth: boolean;
} & object, "startedAt"> & {
    startedAt: number;
}
context
}) =>
context: DistributiveOmit<{
    auth: boolean;
} & object, "startedAt"> & {
    startedAt: number;
}
context
.startedAt: numberstartedAt)

Client Calls

createEffectClient wraps any oRPC client, whether server-side or client-side, so every procedure returns a lazy Effect instead of a promise, ready to yield* inside Effect generators. The output becomes the success value, and errors land in the error channel with their original types preserved, ready for utilities like catchORPCErrorCodes:

import { catchORPCErrorCodes, createEffectClient } from '@orpc/experimental-effect'
import { Effect } from 'effect'

const effectClient = createEffectClient(client)

const program = Effect.gen(function* () {
  const planet = yield* effectClient.planet.find({ id: 1 })
  return planet.name
}).pipe(
  catchORPCErrorCodes({
    NOT_FOUND: error => Effect.succeed('unknown'),
  }),
)

For typed error.data, combine Effect.catchIf with isDefinedError:

import { isDefinedError } from '@orpc/client'
import { Effect } from 'effect'

const recovered = effectClient.planet.find({ id: 1 }).pipe(
  Effect.catchIf(isDefinedError, (error) => {
    // error and error.data are fully typed here
    return Effect.succeed(null)
  }),
)

Effect Schema

oRPC natively supports Standard Schema, and Effect Schema implements that spec through Schema.toStandardSchemaV1:

import { Schema } from 'effect'

const procedure = os
  .input(Schema.toStandardSchemaV1(Schema.Struct({ name: Schema.String })))
  .handler(handlerGen(function* ({ input, context }) {
    return `Hello ${input.name}!`
  }))

.input and .output Extensions

Import @orpc/experimental-effect/extensions/input-output from a module that always runs during initialization, such as the file where you define your base builder or create your server. This lets you define .input and .output directly with Effect Schema:

const procedure = base
  .input(Schema.Struct({ name: Schema.String }))
  .output(Schema.Struct({ greeting: Schema.String }))
  .handler(handlerGen(function* ({ input, context }) {
    return { greeting: `Hello ${input.name}!` }
  }))
import '@orpc/experimental-effect/extensions/input-output'

import { os } from '@orpc/server'

export const base = os

JSON Schema Converter

This integration also provides EffectSchemaToJsonSchemaConverter, which wraps Effect’s built-in Schema.toJsonSchemaDocument. Use it with tools such as the OpenAPI Generator. It accepts the same options as Schema.toJsonSchemaDocument, such as onExcessProperty, generateDescriptions, includeAnnotationKey, and referencePolicy:

import { EffectSchemaToJsonSchemaConverter } from '@orpc/experimental-effect'

const generator = new OpenAPIGenerator({
  converters: [
    new EffectSchemaToJsonSchemaConverter({
      generateDescriptions: true,
    }),
  ],
})

Schemas with an identifier annotation, including Schema.Class and recursive schemas, are preserved in $defs, which OpenAPIGenerator can then hoist into components.schemas.

OpenTelemetry Integration

After setting up the oRPC OpenTelemetry integration, add OtelTracer.layerGlobal from @effect/opentelemetry to the layer behind effect/context, so Effect spans nest under oRPC’s:

npm install @effect/opentelemetry
pnpm add @effect/opentelemetry
yarn add @effect/opentelemetry
bun add @effect/opentelemetry
nub add @effect/opentelemetry
aube add @effect/opentelemetry
import { OtelTracer } from '@effect/opentelemetry'
import { Layer, ManagedRuntime } from 'effect'

const runtime = ManagedRuntime.make(AppLayer.pipe(Layer.provideMerge(OtelTracer.layerGlobal)))
// pass `await runtime.context()` as `effect/context` per request

Cloudflare Workers

Effect recommends a sync scheduler on Workers. Add it to the layer behind effect/context, and create the runtime at module scope:

import { Layer, ManagedRuntime, Scheduler } from 'effect'

const SyncScheduler = Layer.succeed(Scheduler.Scheduler, new Scheduler.MixedScheduler('sync'))

const runtime = ManagedRuntime.make(AppLayer.pipe(Layer.provideMerge(SyncScheduler)))
const effectContext = runtime.context() // pass `await effectContext` per request

Next.js

If you catch defects, for example with Effect.catchCause, call unstable_rethrow first so redirect() and notFound() still reach Next.js:

import { Cause, Effect } from 'effect'
import { unstable_rethrow } from 'next/navigation'

const safeDashboard = loadDashboard.pipe(
  Effect.catchCause((cause) => {
    unstable_rethrow(Cause.squash(cause))
    return Effect.succeed(fallbackDashboard)
  }),
)

Last updated on

Was this page helpful?