better-effect
Start Here

Getting Started

Install better-effect and build a complete typed workflow.

Install the two building blocks

better-effect builds on better-result, so install both packages:

bun add better-effect better-result

The public TypeScript peer range starts at 6.0. The repository uses the latest Bun release by default and the current Node.js LTS for interoperability smoke tests. better-effect also uses the standard Symbol.dispose and Symbol.asyncDispose declarations for disposable resources, so your tsconfig should include a modern JavaScript library such as ESNext.Disposable.

A tiny complete application

This example has one dependency and one program. The type of program records that it needs GreetingService; the Runtime is accepted only when its Layer provides that Service.

import { Result } from 'better-result'
import { Effect, Layer, Runtime, Service } from 'better-effect'

class GreetingService extends Service<GreetingService>()('GreetingService') {
  greet(name: string) {
    return `Hello, ${name}!`
  }
}

const GreetingLive = Layer.make(GreetingService)

const greet = Effect.fn(async function* () {
  const greeting = yield* GreetingService

  return Result.ok(greeting.greet('Ada'))
})

await using runtime = await Runtime.make(GreetingLive)
const result = await runtime.run(greet)

if (Result.isOk(result)) {
  console.log(result.value) // Hello, Ada!
}

Effect.gen evaluates immediately and remains useful when the resolver and Scope are already active. Effect.fn is the recommended Runtime boundary: it captures the generator without running it and returns a nominal Program, so Service resolution starts inside runtime.run. The callback form runtime.run(() => effect) remains supported for existing code.

A Layer's public type is Layer<Provided, Required>: Provided is the tagged Service instance union it creates, and Required is the external dependency union left after composition. Prefer inferred Layers or satisfies Layer<..., ...> when naming a composition root.

MapLayerBackend is the built-in backend and is used by default. Choose a custom backend through Runtime options when integrating a container:

import { ItiLayerBackend } from 'better-effect/adapters/iti'

const runtime = await Runtime.make(GreetingLive, {
  backend: new ItiLayerBackend()
})

MemoryLayerBackend remains an alias of MapLayerBackend from better-effect/testing for compatibility.

Project shape

main.ts

Keep behavior in Services and keep construction in Layers. Your application entry point should be the place where a custom backend, when needed, and the Runtime are selected.

Next steps

  1. Learn how Services become typed dependency tokens.
  2. Compose implementations with Layers.
  3. Understand execution and shutdown in Runtime.
  4. Add ownership to connections and files with Scope and Resource.
  5. Integrate request handlers with Hono.
  6. Call APIs with the HTTP client, including typed responses and streaming boundaries.
  7. Adapt a Kysely database with the Kysely integration.
  8. Adapt a server-side Better Auth instance with Better Auth.

On this page