Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Effect Integration

Effect integration lets you seamlessly use Effect’s powerful features, such as its effect system, concurrency model, and schema library, within oRPC.

Installation

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

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, TCurrentContext extends WithEffectContext<infer S> ? S : never>, TReturn>(handler: HandlerGen<TCurrentContext, TInput, TYield, TReturn, TErrorConstructorMap>): ProcedureHandler<TCurrentContext, TInput, TReturn | Extract<InferYieldError<TYield>, AnyORPCError>, 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>, 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>, never>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 { Effect } from "effect" const delay = (message: string) => Effect.promise<string>( () => new Promise((resolve) => { setTimeout(() => { resolve(message) }, 2000) }) ) // ┌─── Effect<string, never, never> // ▼ const program = delay("Async operation completed successfully!") ```
@see{@link tryPromise} for a version that can handle failures.@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 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, TCurrentContext extends WithEffectContext<infer S> ? S : never>, TReturn>(handler: HandlerGen<TCurrentContext, TInput, TYield, TReturn, TErrorConstructorMap>): ProcedureHandler<TCurrentContext, TInput, TReturn | Extract<InferYieldError<TYield>, AnyORPCError>, 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 { 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 }) ```
@see{@link Reference} for service keys with default values@categoryconstructors@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.
@categorymodels@since2.0.0
Effect
<number>
} >()('MyRandomService') {} 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>, 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>>$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>, never>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>, never>(lazyableProcedure: Lazyable<Procedure<{
    'effect/context': Context.Context<Random>;
}, any, InitialInputSchema, Schema<number>, Record<never, never>, never>>, input: void, options: CallOptions<{
    'effect/context': Context.Context<Random>;
}, Schema<number>, Record<never, 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>, 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 { Context } from "effect" import * as assert from "node:assert" assert.strictEqual(Context.isContext(Context.empty()), true) ```
@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 { Context, pipe } from "effect" import * as assert from "node:assert" 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 }) ) assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) ```
@see{@link addOrOmit} for adding or removing a service from an `Option`@categoryadders@since2.0.0
add
(class RandomRandom, {
next: Effect.Effect<number, never, never>next: import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
Creates an `Effect` that always succeeds with a given value. **When to use** Use when an effect should complete successfully with a specific value without any errors or external dependencies. **Example** (Creating a successful effect) ```ts import { Effect } from "effect" // Creating an effect that represents a successful scenario // // ┌─── Effect<number, never, never> // ▼ const success = Effect.succeed(42) ```
@see{@link fail} to create an effect that represents a failure.@categoryconstructors@since2.0.0
succeed
(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 { Context, Effect } from 'effect'

interface ServerContext extends WithEffectContext<never> {}

export async function fetch(request: Request) {
  const { matched, response } = await handler.handle(request, {
    context: {
      'effect/context': Context.empty(),
      'effect/wrap': (effect, opts) => effect.pipe(
        Effect.catchCause((cause) => {

        })
      ),
    }
  })

  if (matched) {
    return response
  }

  return new Response('Not Found', { status: 404 })
}

Typesafe Errors

When you yield* Effect.fail(new ORPCError(...)) or return new ORPCError(...), oRPC treats it as a returned ORPCError. On the client, you can handle these errors in a typesafe way:

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

  return 'Success'
}))

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

if (isInferableError(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(error.data.id)),
)

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

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, built on top of Effect Schema to JSON Schema. You can use it with tools such as the OpenAPI Generator:

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

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

OpenTelemetry Integration

First, set up the oRPC OpenTelemetry integration. Then instrument your Effect to work seamlessly with OpenTelemetry by providing TracingLive through effect/wrap in the context. This makes Effect tracing equivalent to OpenTelemetry tracing:

import { Resource, Tracer } from '@effect/opentelemetry'
import { Context, Effect, Layer } from 'effect'

interface ServerContext extends WithEffectContext<never> {}

const TracingLive = Tracer.layerGlobal.pipe(
  Layer.provide(Resource.layerFromEnv()),
)

export async function fetch(request: Request) {
  const { matched, response } = await handler.handle(request, {
    context: {
      'effect/context': Context.empty(),
      'effect/wrap': (effect, opts) => effect.pipe(Effect.provide(TracingLive)),
    }
  })

  if (matched) {
    return response
  }

  return new Response('Not Found', { status: 404 })
}

Last updated on August 6, 2026

Was this page helpful?