Composing Effect programs, domain errors, HttpError, repository error types, or error propagation at HTTP boundaries.
When to use: Composing Effect programs, domain errors, HttpError, repository error types, or error propagation at HTTP boundaries.
IMPORTANT: Always consult effect-solutions before writing Effect code.
effect-solutions list to see available guideseffect-solutions show <topic>... for relevant patterns (supports multiple topics)~/.local/share/effect-solutions/effect for real implementationsTopics: quick-start, project-setup, tsconfig, basics, services-and-layers, data-modeling, error-handling, config, testing, cli.
Never guess at Effect patterns - check the guide first.
The Effect v4 repository is cloned to ~/.local/share/effect-solutions/effect for reference.
Use this to explore APIs, find usage examples, and understand implementation
details when the documentation isn't enough.
Effect.gen for sequential effect compositionEffect.tryPromise and typed errorsData.TaggedError for domain-specific error typesEffect.repeat with Schedule for polling/recurring tasksFiber for lifecycle management of long-running effectsEffect programs are instrumented with Effect's native OpenTelemetry support via @effect/opentelemetry. This bridges Effect spans into the existing OTel pipeline (Datadog, etc.) so business logic is visible alongside HTTP request spans.
Every use case function that returns an Effect must be wrapped with Effect.withSpan and annotated with key business IDs:
export const writeScoreUseCase = (input: WriteScoreInput) =>
Effect.gen(function* () {
const parsedInput = yield* parseOrBadRequest(writeScoreInputSchema, input, "Invalid score write input")
yield* Effect.annotateCurrentSpan("score.projectId", parsedInput.projectId)
yield* Effect.annotateCurrentSpan("score.source", parsedInput.source)
// ... business logic
}).pipe(Effect.withSpan("scores.writeScore"))
Rules:
{domain}.{functionName} in camelCase — e.g. scores.writeScore, issues.discoverIssue, evaluations.runLiveEvaluation.yield* Effect.annotateCurrentSpan("key", value) early in the function (after input parsing, before business logic) for key IDs (projectId, scoreId, issueId, etc.) and discriminating attributes (source, status). Only annotate when the value is present (guard nullables).Effect.withSpan is transparent — it does not alter the Effect's success, error, or requirements channels.Effect is already imported in every use case file. withSpan and annotateCurrentSpan are methods on Effect.Every Effect.runPromise call site must include withTracing in the pipe chain to provide the OTel tracer layer:
import { withTracing } from "@repo/observability"
const result = await Effect.runPromise(
myEffect.pipe(
withPostgres(Layer.mergeAll(RepoLive, ...), client, organizationId),
withClickHouse(AnalyticsRepoLive, chClient, organizationId),
withTracing,
),
)
Rules:
withTracing is a pipe combinator exported from @repo/observability. It provides EffectOtelTracerLive — the bridge between Effect's Tracer and the global OTel TracerProvider.withTracing alongside (not inside) infrastructure providers like withPostgres / withClickHouse. Tracing is decoupled from DB layers.withTracing, Effect.withSpan calls are no-ops (Effect's default tracer discards spans). In tests this is fine — tests don't initialize OTel.@hono/otel) are automatically picked up as parents, so Effect spans nest correctly under request traces.Data.TaggedError) instead of raw Error at domain/platform boundariesEffect.either for operations that may fail but shouldn't stop executionHttpError interface (httpStatus and httpMessage), even when the error is not yet surfaced over HTTP—that may change. Use a readonly field for static messages and a getter for messages computed from error fields.@domain/issues)Use packages/domain/issues/src/errors.ts as the gold standard for organizing domain-specific errors:
src/errors.ts; use-cases import from ../errors.ts.@domain/shared errors for generic infrastructure shapes (RepositoryError, generic NotFoundError, etc.).CheckEligibilityError) so Effect error channels stay explicit.docs/issues.md under Domain errors (@domain/issues reference pattern) and in AGENTS.md (domain schema conventions).All domain errors implement the HttpError interface from @repo/utils:
interface HttpError {
readonly _tag: string
readonly httpStatus: number
readonly httpMessage: string
}
Implementation rules:
httpStatus, httpMessage)NotFoundError) instead of nullapp.onError(honoErrorHandler) in server.tsExample domain errors:
// Static message
export class QueuePublishError extends Data.TaggedError("QueuePublishError")<{
readonly cause: unknown
readonly queue: QueueName
}> {
readonly httpStatus = 502
readonly httpMessage = "Queue publish failed"
}
// Dynamic message computed from fields
export class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly entity: string
readonly id: string
}> {
readonly httpStatus = 404
get httpMessage() {
return `${this.entity} not found`
}
}
Example repository method:
findById(id: OrganizationId): Effect.Effect<Organization, NotFoundError | RepositoryError>
Repository method naming (findById vs listByXxx, delete vs softDelete, etc.) is documented in docs/repositories.md. findBy* must not return Entity | null for missing rows — use NotFoundError (or domain-specific not-found) on the error channel; boundaries may catch and map to optional UX when required.