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 catch function to Effect.tryPromise. Without it, errors become an untyped UnknownException, and you lose the ability to handle them with catchTag.

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 ManagedRuntime per 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