Skip to content

Latest commit

 

History

History
330 lines (244 loc) · 14.2 KB

File metadata and controls

330 lines (244 loc) · 14.2 KB

Effect library documentation

This documentation resides in the Effect monorepo, which contains the source code for the Effect library and its related packages.

When you need to find any information about the Effect library, only use this documentation and the source code found in ./packages. Do not use node_modules or any other external documentation, as it may be outdated or incorrect.

Note: The examples in this documentation contain comments for illustration purposes. In practice, you would not include these comments in your code.

Writing Effect code

Prefer writing Effect code with Effect.gen & Effect.fn("name"). Then attach additional behaviour with combinators. This style is more readable and easier to maintain than using combinators alone.

Using Effect.gen

Use Effect.gen to write code in an imperative style similar to async await. You can use yield* to access the result of an effect.

import { Effect, Schema } from "effect"

Effect.gen(function*() {
  yield* Effect.log("Starting the file processing...")
  yield* Effect.log("Reading file...")

  // Always return when raising an error, to ensure typescript understands that
  // the function will not continue executing.
  return yield* new FileProcessingError({ message: "Failed to read the file" })
}).pipe(
  // Add additional functionality with .pipe
  Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)),
  Effect.withSpan("fileProcessing", {
    attributes: {
      method: "Effect.gen"
    }
  })
)

// Use Schema.TaggedErrorClass to define a custom error
export class FileProcessingError extends Schema.TaggedErrorClass<FileProcessingError>()("FileProcessingError", {
  message: Schema.String
}) {}

Using Effect.fn

When writing functions that return an Effect, use Effect.fn to use the generator syntax.

Avoid creating functions that return an Effect.gen, use Effect.fn instead.

import { Effect, Schema } from "effect"

// Pass a string to Effect.fn, which will improve stack traces and also
// attach a tracing span (using Effect.withSpan behind the scenes).
//
// The name string should match the function name.
//
export const effectFunction = Effect.fn("effectFunction")(
  // You can use `Effect.fn.Return` to specify the return type of the function.
  // It accepts the same type parameters as `Effect.Effect`.
  function*(n: number): Effect.fn.Return<string, SomeError> {
    yield* Effect.logInfo("Received number:", n)

    // Always return when raising an error, to ensure typescript understands that
    // the function will not continue executing.
    return yield* new SomeError({ message: "Failed to read the file" })
  },
  // Add additional functionality by passing in additional arguments.
  // **Do not** use .pipe with Effect.fn
  Effect.catch((error) => Effect.logError(`An error occurred: ${error}`)),
  Effect.annotateLogs({
    method: "effectFunction"
  })
)

// Use Schema.TaggedErrorClass to define a custom error
export class SomeError extends Schema.TaggedErrorClass<SomeError>()("SomeError", {
  message: Schema.String
}) {}

More examples

  • Creating effects from common sources: Learn how to create effects from various sources, including plain values, synchronous code, Promise APIs, optional values, and callback-based APIs.

Writing Effect services

Effect services are the most common way to structure Effect code. Prefer using services to encapsulate behaviour over other approaches, as it ensures that your code is modular, testable, and maintainable.

ServiceMap.Service

The default way to define a service is to extend ServiceMap.Service, passing in the service interface as a type parameter.

// file: src/db/Database.ts
import { Effect, Layer, Schema, ServiceMap } from "effect"

// Pass in the service class name as the first type parameter, and the service
// interface as the second type parameter.
export class Database extends ServiceMap.Service<Database, {
  query(sql: string): Effect.Effect<Array<unknown>, DatabaseError>
}>()(
  // The string identifier for the service, which should include the package
  // name and the subdirectory path to the service file.
  "myapp/db/Database"
) {
  // Attach a static layer to the service, which will be used to provide an
  // implementation of the service.
  static readonly layer = Layer.effect(
    Database,
    Effect.gen(function*() {
      // Define the service methods using Effect.fn
      const query = Effect.fn("Database.query")(function*(sql: string) {
        yield* Effect.log("Executing SQL query:", sql)
        return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
      })

      // Return an instance of the service using Database.of, passing in an
      // object that implements the service interface.
      return Database.of({
        query
      })
    })
  )
}

export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()("DatabaseError", {
  cause: Schema.Defect
}) {}

// If you ever need to access the service type, use `Database["Service"]`
export type DatabaseService = Database["Service"]

More examples

Error handling

Error handling basics

Defining custom errors and handling them with Effect.catch and Effect.catchTag.

import { Effect, Schema } from "effect"

// Define custom errors using Schema.TaggedErrorClass
export class ParseError extends Schema.TaggedErrorClass<ParseError>()("ParseError", {
  input: Schema.String,
  message: Schema.String
}) {}

export class ReservedPortError extends Schema.TaggedErrorClass<ReservedPortError>()("ReservedPortError", {
  port: Schema.Number
}) {}

declare const loadPort: (input: string) => Effect.Effect<number, ParseError | ReservedPortError>

export const recovered = loadPort("80").pipe(
  // Catch multiple errors with Effect.catchTag, and return a default port number.
  Effect.catchTag(["ParseError", "ReservedPortError"], (_) => Effect.succeed(3000))
)

export const withFinalFallback = loadPort("invalid").pipe(
  // Catch a specific error with Effect.catchTag
  Effect.catchTag("ReservedPortError", (_) => Effect.succeed(3000)),
  // Catch all errors with Effect.catch
  Effect.catch((_) => Effect.succeed(3000))
)

More examples

Managing resources and Scopes

Learn how to safely manage resources in Effect using Scopes and finalizers.

Running Effect programs

Broadcasting messages with PubSub

Use PubSub when you need one producer to fan out messages to many consumers.

Working with Streams

Effect Streams represent effectful, pull-based sequences of values over time. They let you model finite or infinite data sources.

  • Creating streams from common data sources: Learn how to create streams from various data sources. Includes:

    • Stream.fromIterable for arrays and other iterables
    • Stream.fromEffectSchedule for polling effects
    • Stream.paginate for paginated APIs
    • Stream.fromAsyncIterable for async iterables
    • Stream.fromEventListener for DOM events
    • Stream.callback for any callback-based API
    • NodeStream.fromReadable for Node.js readable streams
  • Consuming and transforming streams: How to transform and consume streams using operators like map, flatMap, filter, mapEffect, and various run* methods.

  • Decoding and encoding streams: Use Stream.pipeThroughChannel with the Ndjson & Msgpack modules to decode and encode streams of structured data.

Integrating Effect into existing applications

ManagedRuntime bridges Effect programs with non-Effect code. Build one runtime from your application Layer, then use it anywhere you need imperative execution, like web handlers, framework hooks, worker queues, or legacy callback APIs.

  • Using ManagedRuntime with Hono: Use ManagedRuntime to run Effect programs from external frameworks while keeping your domain logic in services and Layers.

Batching external requests

Learn how to batch multiple requests into fewer external calls.

Working with Schedules

Schedules define recurring patterns for retries, repeats and polling.

Observability

Effect has built-in support for structured logging, distributed tracing, and metrics. For exporting telemetry, use the lightweight Otlp modules from effect/unstable/observability in new projects, or use @effect/opentelemetry NodeSdk when integrating with an existing OpenTelemetry setup.

Testing Effect programs

Effect HttpClient

Build http clients with the HttpClient module.

Building HttpApi servers

HttpApi gives you schema-first, type-safe HTTP APIs with runtime validation, typed clients, and OpenAPI docs from one definition.

  • Getting started with HttpApi: Define a schema-first API, implement handlers, secure endpoints with middleware, serve it over HTTP, and call it using a generated typed client.

Working with child processes

Use the effect/unstable/process modules to define child processes and run them with `ChildProcessSpawner.

  • Working with child processes: This example shows how to collect process output, compose pipelines, and stream long-running command output.

Building CLI applications

Use the "effect/unstable/cli" modules to build CLI applications. These modules provide utilities for parsing command-line arguments, handling user input, and managing the flow of a CLI application.

Working with AI modules

Effect's AI modules provide a provider-agnostic interface for language models. You can generate text, decode structured objects with Schema and stream partial responses.

Building distributed applications with cluster

The cluster modules let you model stateful services as entities and distribute them across multiple machines.