building-with-effect
Building with Effect
Effect is a powerful TypeScript library for building complex, type-safe programs with composable abstractions for error handling, dependency injection, concurrency, and resource management.
Quick Start
Using Effect.fn (Recommended)
import { Effect, Schema } from "effect";
// Define errors with Schema.TaggedErrorClass
class FetchError extends Schema.TaggedErrorClass<FetchError>()("FetchError", {
message: Schema.String,
}) {}
// Create functions with Effect.fn
export const fetchUser = Effect.fn("fetchUser")(
function* (id: number) {
yield* Effect.logInfo("Fetching user:", id);
// Always return when raising an error
return yield* new FetchError({ message: "Failed to fetch" });
},
// Add combinators as additional arguments (no .pipe needed)
Effect.catch((error) => Effect.logError(`Error: ${error}`)),
Effect.withSpan("fetchUser", { attributes: { method: "Effect.fn" } }),
);
Generator Style
const program = Effect.gen(function* () {
const a = yield* Effect.succeed(10);
const b = yield* Effect.succeed(20);
return a + b;
});
Running Effects
// As Promise
Effect.runPromise(program).then(console.log);
// With NodeRuntime (recommended for apps)
import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program);
// Using Layer.launch as entry point
Layer.launch(WorkerLayer).pipe(NodeRuntime.runMain);
Degrees of Freedom
Match the level of specificity to the task's fragility:
Low Freedom (specific patterns, consistency critical)
These operations have a narrow safe path - follow exactly:
- Error definition: Always use
Schema.TaggedErrorClasswith descriptive_tag - Service structure: Extend
Context.Servicewith staticlayerproperty - Effect.fn usage: Always use
Effect.fn("name")for functions returning Effects - Resource cleanup: Always use
acquireUseReleaseorEffect.addFinalizer - Resource pattern selection: Use
acquireUseReleasefor external resources,Reffor shared mutable state,addFinalizerfor cleanup within existing scope - Error recovery vs propagation: Use
catchTags/orElseto recover; usereturn yield* new Error()to propagate (ALWAYS usereturn yield*to signal the function stops)
Medium Freedom (preferred patterns, some variation acceptable)
These have recommended approaches but context matters:
- Combinator selection: Choose based on need -
catchTagfor specific errors,catchTagsfor multiple,catchfor all - Layer composition: Use
Layer.providefor dependency injection, order matters for overrides - Concurrency control: Use
Effect.allorEffect.forEachwith appropriateconcurrencyoption - Error recovery: Select retry schedules based on failure characteristics
High Freedom (multiple valid approaches, context-dependent)
These depend on application needs:
- Application architecture: Service boundaries, layer organization, entry point structure
- Testing strategies: Test at service level, effect level, or integration level based on needs
- Performance optimization: Caching strategies, batching decisions, bundle size tradeoffs
- Observability setup: Logging granularity, tracing scope, metric selection
Core Type
Effect<Success, Error, Requirements>;
- Success: Value type on success
- Error: Type-tracked errors
- Requirements: Services needed (use
neverif none)
Key Operators
Transformation
map- Transform success valueflatMap/andThen- Chain effectstap- Side effects without changing valuemapError- Transform error type
Error Handling
catch- Handle all errors (renamed fromcatchAllin earlier versions)catchTag- Handle specific error typescatchTags- Handle multiple tagged errors at oncecatchReason/catchReasons- Handle errors with reasonscatchFilter- Handle filtered errors (renamed fromcatchSomein earlier versions)orElse- Fallback effectretry- Retry with policy
Composition
all- Run multiple effectsforEach- Map over collectionzip/zipWith- Combine effectsprovide- Supply dependencies
Best Practices
- Use Effect.fn for functions that return Effects (not Effect.gen alone)
- Define errors with Schema.TaggedErrorClass for type safety
- Use Context.Service for dependency injection
- Build layers explicitly with
Layer.effectand compose withLayer.provide - Use ExecutionPlan for AI provider fallback strategies
- Handle interruptions with
acquireReleasefor resources - Use Layer.launch as application entry point for long-running apps
- Enable dual APIs when appropriate (data-first + data-last)
- Choose resource patterns deliberately: See Resource Pattern Selector
- Use error recovery for resilience: See Error Handling Decision Tree
- Prefer Effect.forEach with concurrency: See Concurrency Anti-Patterns
Workflows
Use these checklists for complex multi-step tasks:
Creating a New Service
Copy this checklist and track progress:
Service Creation Progress:
- [ ] Step 1: Define error types with Schema.TaggedErrorClass
- [ ] Step 2: Create service class extending Context.Service
- [ ] Step 3: Implement methods using Effect.fn
- [ ] Step 4: Build Layer.effect with service implementation
- [ ] Step 5: Provide dependencies via Layer.provide
- [ ] Step 6: Test the service layer
Step 1: Define error types
class ServiceError extends Schema.TaggedErrorClass<ServiceError>()(
"ServiceError",
{ cause: Schema.Defect },
) {}
Step 2: Create service class
export class MyService extends Context.Service<
MyService,
{
method(): Effect.Effect<ReturnType, ServiceError>;
}
>()("namespace/MyService") {}
Step 3: Implement methods
const method = Effect.fn("MyService.method")(function* () {
// Implementation
});
Step 4-6: Build and test layer
See Services & Layers for complete examples.
Setting Up AI Integration
Copy this checklist and track progress:
AI Setup Progress:
- [ ] Step 1: Install provider packages (@effect/ai-openai, @effect/ai-anthropic)
- [ ] Step 2: Configure client layers with API keys
- [ ] Step 3: Define ExecutionPlan for fallback strategy
- [ ] Step 4: Create AI service with Effect.fn
- [ ] Step 5: Implement error handling with mapError
- [ ] Step 6: Provide client layers to service layer
See AI Modules for detailed implementation.
Error Handling Strategy
Copy this checklist and track progress:
Error Handling Progress:
- [ ] Step 1: Define all error types with Schema.TaggedErrorClass
- [ ] Step 2: Use catchTags for multiple specific error handlers
- [ ] Step 3: Add catch for final fallback if needed
- [ ] Step 4: Consider retry with Schedule for transient failures
- [ ] Step 5: Log errors at appropriate layers
- [ ] Step 6: Test error scenarios
See Error Handling for patterns and examples.
Common Patterns
Service with Effect.fn
import { Effect, Context, Layer, Schema } from "effect";
class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()(
"DatabaseError",
{ cause: Schema.Defect },
) {}
export class Database extends Context.Service<
Database,
{
query(sql: string): Effect.Effect<unknown[], DatabaseError>;
}
>()("app/Database") {
static readonly layer = Layer.effect(
Database,
Effect.gen(function* () {
const query = Effect.fn("Database.query")(function* (sql: string) {
yield* Effect.log("Executing SQL:", sql);
return [{ id: 1, name: "Alice" }];
});
return Database.of({ query });
}),
);
}
// Exported type with proper inference
export type DatabaseService = Database["Service"];
Error Handling with catchTags
const configWithFallback = loadConfig().pipe(
Effect.catchTags({
ParseError: () => Effect.succeed(defaultConfig),
FileError: () => Effect.succeed(defaultConfig),
}),
);
Resource Safety
const program = Effect.acquireUseRelease(
openFile("data.txt"),
(file) => processFile(file),
(file) => closeFile(file),
);
Package Structure
Core Package
import { Effect } from "effect";
Unstable Modules (may have breaking changes in minor releases)
import { Schema } from "effect/unstable/schema";
import { HttpClient } from "effect/unstable/http";
import { LanguageModel } from "effect/unstable/ai";
import { PubSub } from "effect/unstable/pubsub";
Platform-Specific Packages (separate packages)
import { NodeRuntime } from "@effect/platform-node";
import { SqlClient } from "@effect/sql-pg";
import { OpenAiClient } from "@effect/ai-openai";
References
Dive deeper into specific topics and patterns:
- Core Patterns - Foundational Effect patterns with Effect.fn
- Error Handling - Schema.TaggedErrorClass, catchTags, catchReason
- Services & Layers - Dependency injection with Context
- Concurrency - Fibers, racing, interruption, coordination
- Data Types - Option, Either, Chunk, HashSet, Stream
- Streams - Creating and consuming streams
- PubSub - Event broadcasting and subscription
- Schedules - Retry, repeat, and scheduling patterns
- AI Modules - LLM integration with tools and chat
- HTTP Client/Server - HttpClient and HttpApi
- Resource Management - Scope, acquire/release patterns
- Schema - Quick start & index
- Observability - Logging, metrics, tracing with Otlp
- Testing - @effect/vitest patterns
- Integration - ManagedRuntime for non-Effect code
- Batching - RequestResolver for batching
- Child Process - Process management
- CLI - CLI application building
- Cluster - Distributed entities
- Migration Guide - Migrating from Effect v3 to v4
Anti-Patterns to Avoid
- Using try/catch with Effect (defeats type safety)
- Mixing Promise-based and Effect-based code without conversion
- Not handling all error cases (use catch or match)
- Ignoring resource cleanup (always use acquireRelease)
- Running effects at module level (breaks composability)
- Using global state instead of Services
- Overusing Effect for simple synchronous operations
- Using Effect.gen alone instead of Effect.fn for functions
Troubleshooting
Type errors with Requirements
- Ensure all services are provided via
Effect.provide - Check Layer composition matches service dependencies
- Use
Effect.provideServicefor quick inline provisions
Effects not executing
- Effects are lazy - must be run with
runPromise,runSync, orrunFork - Check that effect is actually yielded in generator context
Performance issues
- Avoid excessive allocations in hot loops
- Use
Effect.cachedfor expensive computations - Consider
Micromodule for bundle-size sensitive apps
Example Files
Browse detailed examples in the effect-smol/ai-docs/src/ directory:
- Effect Basics - Creating effects, pipe composition
- Services - Context.Service, Layer composition
- Error Handling - catchTags, catchReason, error hierarchies
- Resources - acquireRelease, Scope
- PubSub - Event broadcasting
- Streams - Creating, consuming, encoding
- Integration - ManagedRuntime for non-Effect code
- Batching - RequestResolver patterns
- Schedules - Retry and repeat strategies
- Observability - Logging, tracing, metrics
- Testing - @effect/vitest patterns
- HTTP - HttpClient and HttpApi
- Child Process - Process management
- CLI - CLI application building
- AI - Language models, tools, chat
- Cluster - Distributed entities
More from felixnorden/skills
tdd
Guides test-driven development of software components using the red-green-refactor loop. Covers test doubles, dependency injection, test structure, and naming. Use when writing new features test-first, adding tests to existing code, designing a component's interface through tests, or when the user mentions TDD, unit tests, mocks, stubs, or test structure.
3planning-workflow
Guides structured software planning through reusable templates. Covers design concept authoring, implementation plan authoring with vertical slices, and agent configuration. Use when starting a new feature, designing a solution before implementation, producing a phased plan for a build agent, or when the user mentions planning, design phase, implementation plan, QRSPI, or vertical slices.
3solidity-development
Comprehensive Solidity smart contract development best practices covering security patterns, gas optimization, and professional documentation standards. Use when writing, reviewing, or auditing Solidity smart contracts, or when the user mentions EVM, Ethereum, blockchain development, smart contracts, Solidity, gas optimization, or security audits.
1