一键导入
effect-and-errors
Composing Effect programs, domain errors, HttpError, repository error types, or error propagation at HTTP boundaries.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Composing Effect programs, domain errors, HttpError, repository error types, or error propagation at HTTP boundaries.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Adding or changing routes in `apps/api`. One source of truth (`defineApiEndpoint` + a Zod schema) becomes an HTTP endpoint, an OpenAPI operation, an MCP tool, and a TS SDK method — descriptions and contracts must be written with all four readers in mind.
Layering and boundaries, web vs public API, app layout (clients, routes, logging), ports/adapters, runtime-portable domain/shared/utils code, multi-tenancy, DDD layout, or anti-patterns.
Preparing a production release, pushing a vX.Y.Z release tag, running scripts/release.sh, or updating CHANGELOG.md with the changes that are about to be deployed to production.
Enables or disables Latitude production maintenance mode by redirecting all publicly exposed production services to the Better Stack status page. Use when asked to start, stop, toggle, verify, or prepare a maintenance window.
Adding or reading env vars, updating .env.example, or validating config at startup with parseEnv / parseEnvOptional.
ClickHouse queries, Goose migrations, chdb test schema, or telemetry storage paths.
| name | effect-and-errors |
| description | 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 effectsInside Layer.effect(Tag, Effect.gen(...)), do not store a service read via yield* in a closure that methods use later. Resolve the service again inside each method.
Services bound to request/job scope (anything provided at a boundary per invocation — SqlClient, ChSqlClient, HttpServerRequest, session-scoped auth context) must be resolved per call. If the layer-build closure captures such a service, concurrent callers with different scopes share the first-built reference and silently operate on the wrong context.
// ❌ WRONG — captures the service at layer build; every method uses the stale closure
export const FooRepositoryLive = Layer.effect(
FooRepository,
Effect.gen(function* () {
const sqlClient = yield* SqlClient
return {
save: (x) => sqlClient.query(...),
}
}),
)
// ✅ RIGHT — layer build does not yield the service at all; each method resolves fresh
export const FooRepositoryLive = Layer.effect(
FooRepository,
Effect.gen(function* () {
return {
save: (x) =>
Effect.gen(function* () {
const sqlClient = yield* SqlClient
yield* sqlClient.query(...)
}),
}
}),
)
Do not add a build-time yield* SqlClient as a "dependency assertion" — the dependency is already declared via each method's R channel, and a build-time yield is both redundant and an invitation to accidentally capture the service. The R on port signatures is the single source of truth.
Port method signatures must include scope-bound services in their R channel (e.g. Effect.Effect<A, E, SqlClient>). Mark the service class with @effect-leakable-service to tell the Effect linter this leak is intentional.
Process-singleton services (crypto keys, static config, a queue publisher) can be captured at build. When unsure, resolve per-call.
Effect 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.dev-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 dev-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.