effect-adoption
Adopting Effect in an existing Promise codebase one module at a time.
Skill by andabove
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/andabove-effect-adoption
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/effect-adoption
What's inside
- Skill name
effect-adoption- Description
- Adopting Effect in an existing Promise codebase one module at a time. Use when you migrate an async module to Effect, decide whether a module is worth migrating, put an Effect program behind a plain async function, or review such a migration.
- License
- MIT
- Instructions
- 229 lines in SKILL.md · view on GitHub
Files (2)
- SKILL.md
- effect-adoption-worked-example.md · references/
SKILL.md (first lines)
# Adopting Effect one module at a time
Move one module at a time. Each migrated module keeps its exported async function, so the change stays inside the module and you can revert it alone. The tests move first and do not change during the rewrite. A deliberate break of the new code then proves that the tests can see each behaviour.
Checked against `effect 4.0.0`. When the project has a newer version, read `node_modules/effect/src/<Module>.ts` and `node_modules/effect/AGENTS.md` before you trust a name here.
## Decide first: where Effect pays
| Module shape | Gain |
|---|---|
| One SDK call, and the SDK already has a retry, a timeout and an abort | Little. In a real migration of five server modules, modules like this grew by 18 to 45 percent in lines. |
| Its own retry policy, parallel calls, a concurrency limit, or resources that need cleanup | The most. Effect replaces hand-written loops, timers and `finally` blocks. |
Judge adoption for the codebase, not for each module alone. A team adopts Effect for one pattern everywhere: typed errors, one way to retry and time out, cancel that reaches every call, cleanup that always runs. If the team adopts that pattern, a thin module moves too, and its growth is the known price. If the team does not, migrate only the modules that gain the most.
## Steps
Do the steps in order. Each step ends on its done line.
1. **Measure before.** Record the four [measures](#measure) for the module. Done when you have four numbers from the old code.
2. **Move the tests to the edge.** Point the tests at the function that production calls, through its public signature. Reach its dependencies through the seams it already has, such as a client parameter, so the signature does not change. Write an abort test that checks the call's own signal (see [cancel](#cancel-reaches-the-call)). Commit the tests while they pass on the old code. Run them on the old code several times, for example five, before you claim they pass. One passing run can hide a flaky test. In one migration, a test committed as passing failed in about 2 runs out of 5. Done when the commit is made and every test passes on the old code in every run.
3. **Rewrite inside the module.** Write the Effect program and the [edge](#the-edge-a-plain-async-function). Change no test file. Done when the unchanged tests pass and `tsc` passes.
4. **Break it on purpose.** Make each break below, run the tests, and revert. Done when each break fails at least one test:
- drop the signal from the underlying call,
- change the retry count,
- retry or catch the wrong fault (for example, retry a 404),
- remove the already-aborted guard at the edge.
A break that no test catches is a missing test. Add the test, confirm it fails on the break and passes on the old code, then continue.
5. **Measure after.** Record the four measures again and report both sets. Done when the report holds before and after numbers for each measure.
## The edge: a plain async function
The Effect program stays inside the module. The exported function keeps the old signature, runs the program, and maps the `Exit` back to a value or a thrown error. Callers and tests see no Effect type.
```ts
import * as Cause from "effect/Cause"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Schedule from "effect/Schedule"
interface Invoice {
readonly id: string
readonly total: number
}
interface InvoiceClient {
get(id: string, options: { readonly signal: AbortSignal }): Promise<Invoice>
}
declare const isTransient: (error: unknown) => boolean
export class InvoiceLoadError extends Data.TaggedError("InvoiceLoadError")<{
readonly message: string
readonly cause: unknown
}> {}
const load = (client: InvoiceClient, id: string) =>
Effect.tryPromise({
try: (signal) => client.get(id, { signal }),
catch: (cause) => new InvoiceLoadError({ message: `could not load invoice ${id}`, cause })
}).pipe(
Effect.retry({
times: 2,
schedule: Schedule.spaced("100 millis"),
while: (error) => isTransient(error.cause)
})
)
export async function loadInvoice(
client: InvoiceClient,
id: string,
options: { readonly signal?: AbortSignal } = {}
): Promise<Invoice> {
options.signal?.throwIfAborted()
const exit = await Effect.runPromiseExit(load(client, id), { signal: options.signal })
if (Exit.isSuccess(exit)) return exit.value
if (Cause.hasInterruptsOnly(exit.cause) && options.signal?.aborted) throw options.signal.reason
throw Cause.squash(exit.cause)
}
```
- **Run with `Effect.runPromiseExit(program, { signal })`.** The signal interrupts the fiber when the caller aborts. `runPromiseExit` never rejects, so the edge decides what to throw.
- **Tell a cancel from a fault with `Cause.hasInterruptsOnly(exit.cause)`.** It is `true` only when every reason is an interrupt. Throw the signal's `reason`, so the caller gets back the value it passed to `abort()`.
- **Throw `Cause.squash(exit.cause)` for a fault.** It gives the typed error for a failure and the thrown value for a defect: one error to log. It drops the other reasons in the cause; use `Cause.pretty` when the log needs them all.
- **Keep the error contract the tests assert.** Here the old code threw `InvoiceLoadError` with the fault as `cause`. The tagged error keeps the same `name`, `message` and `cause`, so the old assertions still pass.
- **Keep the program unexported** until a caller also moves to Effect. Then that caller can import the program and compose it, and the edge stays for the remaining Promise callers.
- **Build a layer once, not per call.** When the program needs services, build them once with `ManagedRuntime.make(layer)` at module level and call `runtime.runPromiseExit(program, { signal })`. `Effect.provide(layer)` inside the edge builds the layer again on every call. See `skill:effect-services`.
- **Give that runtime an owner that disposes it.** The runtime holds its resources until `await runtime.dispose()` runs: no release happens before it. Name the code that owns the runtime, and call `dispose()` in the application's shutdown hook and in test teardown (`afterAll`). A run started after `dispose()` fails.
The full module before and after, with its test file and the four breaks, is in [references/effect-adoption-worked-example.md](references/effect-adoption-worked-example.md).
## Cancel reaches the call
Pass the signal that `Effect.tryPromise` gives the thunk to the underlying call: `try: (signal) => client.get(id, { signal })`. When the fiber is interrupted, `runPromiseExit` returns at once and does not wait for the call. A call that did not get the signal runs on to the end, and the code still compiles.
- **Assert the call's own signal in every abort test.** A fake that records the `signal` it receives lets the test check `recorded.aborted === true`. A test that checks only the rejection passes with the signal dropped, because the edge returns at once either way.
- **Declare the parameter in the thunk itself.** Effect makes the `AbortController` only when the thunk's `length` is above 0. A thunk written as `(...args) =>` or `(signal = fallback) =>` has length 0 and gets no fiber signal, though its type says `AbortSignal`.
- **Use `Effect.promise((signal) => ...)` the same way** for a call that cannot reject.
## A signal that is already aborted
`runPromiseExit` runs the program up to its first async step (or until the runtime yields, after a long run of sync steps) and only then reads the signal. So with a signal that is already aborted:
- a program that starts with the call starts the call, with a signal that is not yet aborted, then aborts it;
- a program with sync steps first runs them, then starts the call the same way;
- a program with an async step before the call does not start the call;
- a program with no async step completes and returns its value.
Do not promise that no call starts. If the old function guaranteed it, keep the guard `options.signal?.throwIfAborted()` at the top of the edge, as in the example, and keep a test for it.
## Errors keep their cause
Wrap each fault in a `Data.TaggedError` with a `cause` field that holds the original fault. `Data.TaggedError` passes `cause` and `message` to the native `Error` constructor, so `error.cause` is the original fault, and log sanitisers and error reporters that walk the `cause` chain still name the real fault.
- Give the error a `message` field. Without one, `error.message` is the empty string.
- The error's `name` is its tag, so assertions on `name` keep working when the tag is the old class name.
- Name the field `cause`. Only that name reaches the native `Error.cause` that reporters walk.
For tagged errors, `catchTag`, and retry policy as error handling, see `skill:effect-errors`.From andabove/skills, MIT.
More from andabove/skills
- 0.570score
effect-testing
andabove
Testing Effect code with @effect/vitest, TestClock and test layers.
SkillAFreshness: AClaude Code +9 - 0.570score
effect
andabove
Effect (the TypeScript library) core and router to the other Effect skills.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-concurrency
andabove
Effect concurrency - fibers, interruption, coordination and Schedule.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-data
andabove
Effect 4 data types, equality, caching and batching.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-errors
andabove
Effect error handling, for expected errors, defects and interruptions.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-observability
andabove
Effect 4 logging, tracing and metrics, and their export.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-schema
andabove
Effect 4 Schema for parsing and modelling data.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-services
andabove
Effect 4 services, layers and resources.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-streams
andabove
Effect Stream, Sink and Channel.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-v3-to-v4
andabove
Effect v3 to v4 migration.
SkillAFreshness: AClaude Code +9 - 0.565score
linear-comment
andabove
Write a comment on a Linear issue as a status record against its acceptance criteria and QA notes.
SkillAFreshness: AClaude Code +9 - 0.565score
code-style
andabove
Fifteen TypeScript code rules for modules, names, doc comments, types, boundaries, errors and side effects.
SkillAFreshness: AClaude Code +9
Signals
- Stars
- 0
- Forks
- 0
- Open issues
- 0
- Last commit (this folder)
- Oct 3, 2026 (26 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/andabove/skills
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from andabove
- 0.570score
effect-errors
andabove
Effect error handling, for expected errors, defects and interruptions.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-data
andabove
Effect 4 data types, equality, caching and batching.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-testing
andabove
Testing Effect code with @effect/vitest, TestClock and test layers.
SkillAFreshness: AClaude Code +9 - 0.570score
effect
andabove
Effect (the TypeScript library) core and router to the other Effect skills.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-concurrency
andabove
Effect concurrency - fibers, interruption, coordination and Schedule.
SkillAFreshness: AClaude Code +9 - 0.570score
effect-observability
andabove
Effect 4 logging, tracing and metrics, and their export.
SkillAFreshness: AClaude Code +9
Cite this page
effect-adoption skill. Agent Mart. https://agentmart.dev/skills/andabove-effect-adoption (last updated 2026-10-24)