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@betapnpm add @orpc/experimental-effect@beta effect@betayarn add @orpc/experimental-effect@beta effect@betabun add @orpc/experimental-effect@beta effect@betaEffectful 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.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.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.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!")
```promise(() => var Promise: PromiseConstructorRepresents the completion of an asynchronous operationPromise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)Creates a new resolved promise for the provided value.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 = osEffect 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.handlerGen, interface WithEffectContext<Services>A context shape that provides Effect services to effectful handlers
through the oRPC context in a typesafe way.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 })
```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.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.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.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.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.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)
```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 })
```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)
```succeed(var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): numberReturns 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 = osJSON 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 })
}