Hyperlinkv0.8.0-beta.28

Effect Style

How Effect code reads day to day — the platform surface and idioms, plus formatting and comments.

Reach for native Effect subpaths, not external packages

srcexamples

Reactivity, RPC, SQL, HTTP, event logs, persistence — all ship inside Effect under effect/unstable/*. Import them from there; never pull an external package that duplicates them (no @effect-atom — reactivity is effect/unstable/reactivity).

// ✅ good — native
import * as Reactivity from "effect/unstable/reactivity"
import { RpcServer } from "effect/unstable/rpc"
import { SqlClient } from "effect/unstable/sql/SqlClient"

// ❌ bad — an external package that Effect already provides
import { Atom } from "@effect-atom/atom"

Know the surface before reaching outside it

srcexamples

Effect is large — most of what youd pull a dependency for already ships. Scan here before adding one.

Core (effect):

  • Runtime & structureEffect, Layer, Context, Runtime, ManagedRuntime, Scope, Hyperlink

  • State & concurrencyRef, SubscriptionRef, Deferred, Queue, PubSub, Fiber (FiberHandle / FiberMap / FiberSet), Pool

  • Data & errorsData, Cause, Exit, Option, Result, Match, Predicate, Schema (SchemaAST, SchemaIssue, …)

  • Time & schedulingClock, Duration, DateTime, Schedule, Cron

  • StreamingStream, Channel

  • Config & observabilityConfig, ConfigProvider, Metric, Logger, Tracer

  • PlatformFileSystem, Path, PlatformError, Crypto

Families (effect/unstable/*), each a self-contained subpath (see Public vs internal → domain family): rpc, sql, http, httpapi, cli, reactivity, persistence, eventlog, encoding, socket, workers, workflow, cluster, observability, ai.

Where to look. The authoritative source for the version we run is node_modules/effect/src/<Module>.ts; the vendored repos/effect/packages/effect/src mirrors it for browsing. Open the real module before guessing an API (next rule).

A payload slot accepts any Schema.Top, not only loose fields

src

Payload, input, and wire slots are typed Schema.Top, so they take a single schema value of any kind — Schema.Struct, Schema.Class, Schema.Array, a union — and the loose-fields shorthand ({ id: Schema.String }), which is sugar that wraps into a struct and is idiomatic in RPC method definitions. Both are valid.

The rule is what a payload-accepting API must accept: always a full Schema.Top, never only loose fields. Loose-only is the trap that bit the queue input — it blocked passing a named schema or a Schema.Class. Accept the full schema and the shorthand falls out for free.

// ✅ all valid — the slot is Schema.Top
payload: Schema.Struct({ id: Schema.String })   // a struct
payload: WorkItem                                // a Schema.Class
payload: Schema.Array(itemSchema)                // an array, a union, …
payload: { id: Schema.String }                   // loose-fields shorthand — fine, wraps to a struct

// ❌ bad — an API that accepts ONLY loose fields; a named schema can't be passed
declareQueue(fields: Record<string, Schema.Top>)

Errors that cross the wire are schema errors

srcexamples

An in-process failure is a Data.TaggedError (see Principles → Errors are values). An error that travels over RPC must also encode, so it extends the schema error class (Schema.TaggedErrorClass) — that makes it both a yieldable error and wire-serializable in one declaration.

// ✅ good — wire-encodable: yieldable AND serializable
class QueueMissingItemSchemaError extends Schema.TaggedErrorClass<QueueMissingItemSchemaError>()(
  "QueueMissingItemSchemaError",
  { id: Schema.String },
) {}

Use Effect platform services, not raw node:*

srcexamples

Filesystem, path, process, and HTTP work goes through the Effect service — FileSystem, Path, ChildProcess, HttpClient — never a raw node:* import. If no service exists for a primitive (for example Ed25519 crypto), isolate the Node API behind a small Effect-returning helper rather than scattering raw calls.

// ❌ bad — raw node
import { readFile } from "node:fs/promises"
const text = await readFile(path, "utf8")

// ✅ good — the FileSystem service
const fs = yield* FileSystem.FileSystem
const text = yield* fs.readFileString(path)

Read the resolved Effect source before guessing

src

When youre unsure of an Effect API, open the resolved package (node_modules/effect/src, or the vendored repos/effect/packages/effect/src) and copy the real shape — dont guess from memory. repos/ is read-only reference: never import from it, never edit it. Application and package code import from the declared effect dependency.

Never nest a yield* inside another expression

srcexamplestest

A yield* stands on its own — bind its result to a const, then use it. Never tuck a yield* into another calls arguments or an expression; thats what a const (or a pipe) is for — see Principles → Pipe, dont wrap.

// ❌ bad — a yield* wrapped inside another call
yield* emails.add(yield* nextEmail)

// ✅ good — pipe it, no throwaway const
yield* nextEmail.pipe(Effect.andThen(emails.add))

// ✅ also fine — bind to a const, then use
const email = yield* nextEmail
yield* emails.add(email)

One field per line

srcexamplestest

Never collapse a multi-field object or parameter list onto one line. One field per line, always — a collapsed literal is unreadable on a narrow screen and buries a bad diff.

// ❌ bad — collapsed onto one line
const config = { levelCount: 4, namedLevels: { interactive: 0, batch: 3 }, takeAlgorithm: "weighted" }

// ✅ good — one field per line
const config = {
  levelCount: 4,
  namedLevels: { interactive: 0, batch: 3 },
  takeAlgorithm: "weighted",
}

Doc comments live in their own chapter — how a doc comment is shaped, the @public / @internal / @module markers, {@link} cross-refs, and @example rules are all in Documentation.

Edit this page on GitHub