Skip to content

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

You Should Use Effect: Demo Snippets

Small, standalone snippets for a video about the Effect TypeScript library. Each file demonstrates one idea and can be run on its own.

Uses Effect 4 (release candidate), pinned to effect@4.0.0-rc.117 (see Heads-up).

Setup

Requires Node.js 22.18+, which runs .ts files natively (no build step, no tsx).

npm install
npm run prepare                  # patches TypeScript with the Effect language service (see below)
node src/02-handling-errors.ts   # run any demo
npm run typecheck                # type-check all demos (incl. Effect-specific lint rules)

npm install normally runs prepare automatically. It doesn't if npm's ignore-scripts setting is enabled (as it was on the machine this was set up on). In that case, run npm run prepare once after installing.

Recording tips

  • Hover over variables in your editor. The type comments (┌─── Effect<...>) in the files match what the compiler infers. Every one of them was checked.
  • No explicit return types: everything is inferred, so what you see on hover is exactly what TypeScript knows. With Promise<User>, the possible errors are simply missing.
  • src/lib/fake-clients.ts holds small fake clients (database, exchange-rate API, storage, moderation API). They return real Promises with simulated latency, so the demos call them like you'd call Prisma, fetch or the S3 SDK.
  • Lines under a // ❌ Uncomment ... comment are intentional mistakes, kept commented out so npm run typecheck passes. Uncomment one on camera to show the red squiggle.

Demo Index

File Reason Shows Replaces
00-effect-is-a-value.ts Foundation Effects are lazy values: nothing runs until you run it, so you can enhance it first –
01a-typed-errors-without-effect.ts #1 Type safety getUser is inferred as Promise<User>: the failures are invisible –
01b-typed-errors-with-effect.ts #1 Type safety The same getUser as Effect<User, InvalidId | DatabaseError | UserNotFound> (the rejected DB Promise mapped to a typed error) try/catch + unknown
02-handling-errors.ts #1 Type safety catchTag removes errors from the type; typos and double handling don't compile –
03-errors-vs-defects.ts #1 Fine print Only expected errors are typed; defects (bugs) are not –
04-services.ts #2 Dependencies Requirements are part of the type; you can't run a program with missing deps manual DI / singletons
05-layers.ts #2 Dependencies Layers build services; the dependency graph is checked; swap live/test DI containers (InversifyJS…)
06-retry-and-timeout.ts #3 Built-in Exponential backoff + jitter + typed retry condition; timeout as a typed error or a fallback p-retry, p-timeout
07-concurrency.ts #3 Built-in forEach with concurrency: 3 p-limit, p-map
08-queue.ts #3 Built-in Bounded queue with back-pressure, producer and consumer p-queue, fastq
09-rate-limiter.ts #3 Built-in 2 calls/sec (delay mode) and a typed error (fail mode) bottleneck, limiter
10-schema.ts #3 Built-in One schema gives you a type, validation, and transformation, with all errors reported zod, valibot
11-config.ts #3 Built-in Typed env config, defaults, redacted secrets, typed ConfigError dotenv + zod + manual parsing
12a-structured-concurrency-without-effect.ts Bonus Promise.all: the upload keeps running after validation failed –
12b-structured-concurrency-with-effect.ts Bonus Effect.all: the upload is interrupted, and the storage request itself is aborted via AbortSignal –
13-resource-safety.ts Bonus acquireRelease: cleanup always runs; Scope appears in the type try/finally everywhere
14-effect-language-service.ts Bonus @effect/tsgo flags valid-TS-but-wrong-Effect code: never-run effects, untyped Error, v3 APIs –

Run 11-config.ts with different env vars:

node src/11-config.ts                                  # missing API_KEY -> typed ConfigError
API_KEY=secret-123 PORT=8080 node src/11-config.ts     # valid
API_KEY=secret-123 PORT=abc node src/11-config.ts      # invalid port

Style notes. The demos use Data.TaggedError for errors because it's the least noisy option. Effect's own agent guide (LLMS.md) prefers Schema.TaggedError, which gives serializable, schema-validated errors. Reusable functions use Effect.fn("name")(function* () {...}), the v4 idiom. It works like async function and also creates a tracing span.


My Take on Your Three Points

1. "Type-safe programs, not just type-safe data": ✅ valid, and your strongest point

Effect's own v4 docs open with this framing: "Typed data, untyped programs". Effect<Success, Error, Requirements> describes what a program produces, how it can fail, and what it needs.

This goes beyond "errors in the signature". The compiler tracks your error handling as you go:

  • catchTag removes an error from the type.
  • timeout adds TimeoutError.
  • Once everything is handled, the error type becomes never (demos 02, 06).

Resources work the same way: Scope shows up in the type (demo 13).

Caveats worth saying on camera, so the video stays honest:

  • Only expected errors are typed. Throwing inside Effect.sync or a rejected Effect.promise becomes a defect, which is invisible in the type (demo 03). This is intentional: bugs aren't domain errors. But "every error is typed" would be an overclaim.
  • Your error types are only as good as your boundaries. Effect.tryPromise(() => fetch(url)) without a catch mapper gives you a generic UnknownError. You get precise types only if you map errors at the edges.
  • Tag strings must be unique. If two error classes use the same _tag, neither the compiler nor the runtime warns you. catchTag catches both.
  • In generators, write return yield* new MyError(). Without the return, TypeScript won't narrow after the failure.

2. "Type-safe dependencies + clear structure (helps AI agents)": ✅ valid, but it needs nuance

What's true:

  • Required services show up in the type.
  • You can't run a program until everything is provided (demo 04).
  • Layers make the dependency graph explicit, and the compiler checks it's complete (demo 05).
  • Swapping implementations for tests is trivial.

The Effect team leans hard into the AI angle:

  • The docs say "Built for the AI era".
  • The npm package ships AGENTS.md / CLAUDE.md.
  • The repo has an agent guide (LLMS.md plus runnable ai-docs/ examples).
  • The Effect language service (@effect/tsgo, set up in this project, see below and demo 14) adds Effect-specific diagnostics to tsc, such as never-run effects and outdated v3 APIs. Agents see these like any other compiler error.

Where I'd push back:

  • "Clear rules about how programs should be structured" is a bit strong. Effect is opinionated: services, layers, tagged errors, Effect.fn. But it's a toolkit, not a framework that enforces an architecture, and there are often several ways to do the same thing:

    • generators vs. pipe
    • Data.TaggedError vs. Schema.TaggedError
    • Layer.succeed vs. Layer.effect

    What is enforced is that errors and dependencies must be declared. That's the part that actually helps agents: they can't silently forget a dependency or swallow an error without the types showing it. Pitch it that way.

  • The big AI pitfall right now: v3 → v4 renamed a lot. Most LLM training data is v3 code, so out of the box, agents write v3 code that doesn't compile on v4. The compiler catches it (type safety pays off again), but expect friction. The Effect team's own advice is to vendor the Effect source into your repo (git subtree) and point the agent at LLMS.md (blog post).

    v3 v4
    Effect.catchAll Effect.catch
    Context.Tag / Effect.Service Context.Service
    Either Result
    Effect.fork Effect.forkChild
    Effect.async Effect.callback
    UnknownException UnknownError
    @effect/platform, @effect/sql, … mostly inside effect itself (see below)
  • A human still has to review the AI's output. With Effect, that requires knowing Effect. Agents lowering the barrier to writing Effect doesn't lower the barrier to reviewing it.

3. "Lots of features built in, fewer third-party libraries": ✅ valid, even more so in v4

v4 consolidated the ecosystem into the single effect package. That includes:

  • HTTP client/server
  • HttpApi (schema-first APIs with OpenAPI)
  • SQL, RPC, CLI
  • AI (LLMs, tools, MCP)
  • workflows, cluster
  • persistence (RateLimiter, KeyValueStore)
  • OTLP observability

Runtime and driver packages remain separate: @effect/platform-node, @effect/sql-pg, @effect/ai-anthropic, and so on.

The stronger argument isn't "fewer dependencies", it's "they all compose." Retry, timeout, queue, rate limiter, schema and config share one error channel, one interruption model, one DI system, and one logging/tracing system. p-retry + p-limit + bottleneck + zod + dotenv each have their own error model, and none of them can cancel the others.

Caveats:

  • Some of it is officially unstable. In the RC, RateLimiter, HTTP, SQL, AI and CLI live under effect/unstable/* and may break in minor releases. The core modules used in demos 06–08 and 10–11 are stable: Effect, Schedule, Queue, Semaphore, Cache, Schema, Config, Stream.
  • Effect's RateLimiter is a keyed, store-backed limiter. It uses an in-memory or Redis store, so it can rate-limit across processes. For simple in-process throttling, the concurrency option (demo 07) or a Semaphore is often all you need.
  • "Built in" doesn't mean every module beats the best standalone library in its niche (e.g. zod's ecosystem). Schema does support Standard Schema, which helps with interop.

What You Might Have Missed

  1. Structured concurrency and interruption (demos 12a/12b). Probably the most underrated feature. Promises can't be cancelled. In Effect, a failure in Effect.all, a timeout, or a lost race actually stops the other work. Wrapped promises receive an AbortSignal (Effect.tryPromise((signal) => fetch(url, { signal }))), so real requests get aborted too.
  2. Programs as values (demo 00). This is the foundation that makes retries, timeouts and concurrency "just functions you apply". I'd explain it first. It's the mental shift everything else depends on.
  3. Resource safety (demo 13). acquireRelease guarantees cleanup on success, failure, and interruption. Scope in the type means you can't forget to define a resource's lifetime.
  4. Testability. Beyond swapping layers (demo 05), TestClock from effect/testing lets you test time-based code, like a retry policy with minutes of backoff, instantly. For Vitest integration there's @effect/vitest (it.effect).
  5. Observability is built in. Effect.fn("name") creates tracing spans automatically. Structured logging (the [time] INFO (#fiber) lines you see in the demos), metrics and an OTLP exporter ship in the box.
  6. Incremental adoption. You don't have to rewrite your app. Run effects at the edges (Effect.runPromise, or ManagedRuntime inside an Express/Hono handler) and grow from there. This is a big deal for viewers with existing codebases.

Pitfalls and Honest Downsides

  • Learning curve. Effect brings a lot of new vocabulary: fibers, layers, scopes, schedules, yield*. It feels like a new way of writing TypeScript, and your team needs to buy in.
  • It spreads. Once functions return Effects, callers want to be Effects too. You need clear boundaries where Effect code meets non-Effect code.
  • Overkill for small things. The payoff grows with complexity: many failure modes, concurrency, external integrations. For a 50-line script or a simple CRUD app, it's hard to justify.
  • Bundle size. It tree-shakes, but there's a baseline runtime cost. Effect's own myths page cites ~25 KB gzipped as the minimum. That matters for frontends, rarely for backends.
  • Performance is a common objection, but a weak one for app-level code (same myths page). The "500× slower" claims come from 1 + 1 micro-benchmarks.

Heads-up: v4 Is an RC

  • Tutorials, StackOverflow answers and LLM knowledge are mostly v3. The docs for v4 are at effect.website/docs/v4.
  • Import paths are about to change. A pending change on Effect's main branch moves effect/unstable/* to effect/*: effect/unstable/persistence becomes effect/persistence, effect/unstable/http becomes effect/http. The modules stay marked unstable. That's why this project pins 4.0.0-rc.117. If you upgrade, only the import in 09-rate-limiter.ts should need changing.

@effect/tsgo: The Effect Language Service

@effect/tsgo is TypeScript that knows the rules of Effect. TypeScript only checks types. It doesn't know Effect's rules, so plenty of code is valid TypeScript but broken Effect code: an effect that's created but never run, a v3 API, running an effect inside another effect. @effect/tsgo is Effect's build of TypeScript 7 ("TypeScript-Go", Microsoft's native compiler) with 100+ Effect-specific rules compiled in. Its diagnostics show up in your editor and in plain tsc output, so an agent running npm run typecheck gets told "this is the v3 API catchAll, it was renamed to catch" instead of guessing.

It's set up here via npx @effect/tsgo setup. That added the tsconfig.json plugin entry, the prepare script (effect-tsgo patch, which patches the TypeScript binary in node_modules) and .vscode/settings.json. For editor squiggles, the setup tool says to install the TypeScript 7 extension and make sure the native TS server is active. Each release supports specific TS versions (0.45.0 supports 7.0.2), which is why typescript is pinned.

The same mistakes, run through plain TS 7 and through @effect/tsgo (demo 14):

Mistake Plain TypeScript @effect/tsgo
Effect.log(...) without yield* (never runs!) nothing ❌ "This Effect value is neither yielded nor used in an assignment."
Effect.fail(new Error("Payment declined")) nothing ⚠️ "Global 'Error' loses type safety ... Consider using a tagged error"
Effect.catchAll(...) (v3 API) "Property 'catchAll' does not exist ..." ⚠️ "... uses the Effect v3 API catchAll ... Renamed to catch."
Running a program with a missing service (demo 04) "Effect<void, never, Mailer | UserRepo> is not assignable to Effect<void, never, never>" ❌ "This Effect requires a service that is missing ...: Mailer | UserRepo"

Guard Rails Against Effect Bad Practices

Every rule has a severity: error, warning, suggestion or off. Errors and warnings fail tsc. Suggestions are only printed. Several important anti-patterns are just suggestions by default, so neither CI nor an agent is forced to fix them. This project raises them to error in tsconfig.json:

Rule Catches
runEffectInsideEffect Effect.runPromise/runSync inside an Effect (see below)
tryCatchInEffectGen try/catch inside Effect.gen: bypasses typed error handling
returnEffectInGen return someEffect instead of return yield* someEffect: a nested Effect<Effect<…>> that never runs
leakingRequirements Service methods that expose their dependencies (the rule behind demo 05)
catchUnfailableEffect Error handling on an Effect that can't fail (dead code / wrong assumption)

Why running effects inside effects is bad (all verified at runtime). Effect.runPromise inside an Effect starts a separate, detached program:

  • Cancellation is lost. An outer timeout fired, but the nested run kept going and finished anyway.
  • Typed errors become untyped. A typed PaymentDeclined failure arrived as an untyped defect (Die).
  • Context is lost. Log annotations (e.g. a requestId), tracing spans and provided services don't carry over. You'd have to provide dependencies again, e.g. rebuild a DB pool per call.

The fix is almost always a plain yield* (or Effect.forkChild for background work). Effect.runPromise belongs at the edges of your app.

More options:

  • The rules are grouped into categories: correctness, anti-pattern, effect-native, style. Turn on more of them via diagnosticSeverity in tsconfig.json.
  • npx @effect/tsgo setup --preset effect-native enables 22 "use the Effect way" rules: Effect.log over console.log, Config over process.env, no async functions, … That's great for real Effect apps. It's not used here because the demos compare against plain TS on purpose.
  • strictEffectProvide ("provide layers only at the entry point") is useful in apps but flags every entry point in these standalone demos.
  • An alternative is the official Oxlint integration (oxlint + oxlint-tsgolint). It ships ready-made presets (recommended, correctness, antipattern, effect-native, style) and runs as a separate lint step (setup guide).

Suggested Video Flow

  1. Hook: "TypeScript types your data, not your programs."
  2. Foundation: 00
  3. Reason #1: 01a vs. 01b (split screen) → 02, then 03 as the honesty moment
  4. Reason #2: 04 → 05, plus the AI angle and the v3/v4 caveat
  5. Reason #3: 06–11 rapid-fire, framed as "this replaces library X"
  6. What people miss: 12a vs. 12b, 13, 14 (14 fits well with the AI angle of reason #2)
  7. Honest downsides + when not to use Effect

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages