Integrate with Existing Code
ManagedRuntime: The Bridge
import { Layer, ManagedRuntime, Effect } from "effect"
// Your application's layer tree
const MainLive = Layer.empty // replace with your actual layers
const runtime = ManagedRuntime.make(MainLive)
// Use from any non-Effect code
async function handler() {
const result = await runtime.runPromise(myEffectProgram)
return result
}
ManagedRuntime.make builds a runtime from your layer tree. Call runtime.runPromise(effect) from non-Effect code to execute an Effect and get a Promise back. Build the runtime once at startup, reuse it everywhere.
Hono Integration
import { Hono } from "hono"
import { Context, Effect, Layer, ManagedRuntime, Schema } from "effect"
// --- Service Definition ---
class UserService extends Context.Service<UserService, {
getById(id: number): Effect.Effect<{ id: number; name: string }, never, never>
}>()("myapp/UserService") {
static readonly Live = Layer.sync(
UserService,
UserService.of({
getById: (id) => Effect.succeed({ id, name: `User ${id}` })
})
)
}
// --- Effect Program ---
const getUser = (id: number) =>
Effect.gen(function*() {
const service = yield* UserService
return yield* service.getById(id)
})
// --- Build Runtime Once ---
const runtime = ManagedRuntime.make(UserService.Live)
// --- Hono App ---
const app = new Hono()
app.get("/users/:id", async (c) => {
const id = Number(c.req.param("id"))
const user = await runtime.runPromise(getUser(id))
return c.json(user)
})
export default app
The Hono handler calls runtime.runPromise to enter the Effect world. Your domain logic stays in Effects and services. The framework boundary is one line.
Express Integration
import express from "express"
import { Context, Effect, Layer, ManagedRuntime } from "effect"
class Database extends Context.Service<Database, {
query(sql: string): Effect.Effect<unknown[], never, never>
}>()("myapp/Database") {
static readonly Live = Layer.sync(
Database,
Database.of({
query: () => Effect.succeed([{ id: 1, name: "Alice" }])
})
)
}
const runtime = ManagedRuntime.make(Database.Live)
const app = express()
app.get("/users", async (req, res) => {
const users = await runtime.runPromise(
Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
)
res.json(users)
})
app.listen(3000)
Wrap Existing Promises with Effect.tryPromise
import { Effect, Schema } from "effect"
class DbError extends Schema.TaggedError<DbError>()("DbError", {
message: Schema.String
}) {}
// Enter the Effect world from a Promise
const findUser = (id: number) =>
Effect.tryPromise({
try: () => db.raw("SELECT * FROM users WHERE id = ?", [id]),
catch: (error) => new DbError({ message: String(error) })
})
Effect.tryPromise wraps a Promise-returning function into an Effect. The try function produces the Promise. The catch function maps any rejection into a typed error.
Gotcha: Always provide a
catchfunction toEffect.tryPromise. Without it, errors become an untypedUnknownException, and you lose the ability to handle them withcatchTag.
Exit the Effect World with Effect.runPromise
import { Effect } from "effect"
// Full circle: wrap a Promise, process in Effect, return a Promise
const program = Effect.gen(function*() {
const user = yield* findUser(1)
return user
})
// Back to Promise-land
Effect.runPromise(program)
.then((user) => console.log(user))
.catch((err) => console.error(err))
Gradual Adoption Strategy
Start with a thin Effect layer for new code while keeping existing code as-is:
import express from "express"
import { Effect, Layer, ManagedRuntime, Context } from "effect"
// 1. Define services for new features
class EmailService extends Context.Service<EmailService, {
send(to: string, subject: string, body: string): Effect.Effect<void, never, never>
}>()("myapp/EmailService") {
static readonly Live = Layer.sync(
EmailService,
EmailService.of({
send: (to, subject, body) =>
Effect.tryPromise({
try: () => sendEmailApi(to, subject, body),
catch: () => Effect.fail("email failed" as never)
})
})
)
}
const runtime = ManagedRuntime.make(EmailService.Live)
const app = express()
// 2. New endpoints use Effect
app.post("/notify", async (req, res) => {
await runtime.runPromise(
Effect.gen(function*() {
const email = yield* EmailService
yield* email.send(req.body.to, req.body.subject, req.body.body)
})
)
res.json({ ok: true })
})
// 3. Existing endpoints stay untouched
app.get("/legacy", (req, res) => {
legacyHandler(req, res)
})
Tip: Build one
ManagedRuntimeper process. Creating a runtime has overhead (layer construction, context building). Share it across all handlers.
Wrapping Callback-Based APIs
import { Effect } from "effect"
// Convert a callback API to an Effect
const readFileEffect = (path: string) =>
Effect.async<string, Error>((resume) => {
fs.readFile(path, "utf-8", (err, data) => {
if (err) resume(Effect.fail(err))
else resume(Effect.succeed(data))
})
})
Effect.async bridges callback-based APIs. Call resume with an Effect.succeed or Effect.fail when the callback fires. The Effect type system tracks success and error types.
Next Steps
- Define Services - Structure your domain logic in services
- Test Effect Programs - Test the Effect code you’ve integrated