Fix: Effect (effect-ts) Not Working — Effect Not Running, Services Missing, or Type Errors
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Effect issues — running effects with Effect.runPromise, Layer and service dependency injection, error channel types, Fiber concurrency, Schema validation, and common TypeScript errors.
The Problem
You create an Effect but nothing happens:
import { Effect } from 'effect';
const program = Effect.log('Hello');
// Nothing prints — the effect never runsOr providing a service fails with a type error:
Effect.runPromise(program);
// Type error: Argument of type 'Effect<void, never, UserService>'
// is not assignable to parameter of type 'Effect<void, never, never>'Or an Effect fails at runtime with a “service not found” error:
Error: Service not found: UserServiceOr error handling compiles but the error channel type is wrong:
const result = Effect.tryPromise(() => fetch('/api'));
// Type is Effect<Response, UnknownException, never>
// but you want Effect<Response, FetchError, never>Why This Happens
Effect is a functional effect system for TypeScript. Every Effect<A, E, R> is a description of a computation, not the computation itself — it only runs when you explicitly execute it. The mental shift is similar to going from Promise (eager, runs immediately) to a lazy IO monad — and most production bugs come from treating Effect like Promise.
The three type parameters are load-bearing in a way Promise’s single T is not. A is the success type, E is the typed error channel, and R is the dependency channel. The compiler refuses to run an effect with unsatisfied dependencies, so a “type error at runPromise” almost always means a service somewhere in your call graph hasn’t been provided. This is good — it’s the compiler saving you from a Service not found exception in production — but it makes the diagnostic cycle longer because you have to walk back through the layer composition to find what was missed.
The runtime layer adds another category of issues. Effect ships an ESM-only package with optional fiber-based concurrency, and that combination interacts differently with each JavaScript runtime. Node, Bun, and Deno expose subtly different microtask schedulers, and bundler choices (Webpack vs esbuild vs tsx) can break the lazy evaluation by inlining what should stay deferred. Frontend integrations add their own complications: the browser bundle is large, React rendering doesn’t naturally compose with Effect.gen, and integrating with React’s lifecycle requires @effect-rx/rx-react or hand-rolled hooks.
- Effects are lazy values —
Effect.log('Hello')creates an effect description. Nothing happens until you pass it toEffect.runPromise,Effect.runSync, orEffect.runFork. This is the most fundamental difference from imperative code. - The
Rtype parameter tracks service dependencies — when your effect uses a service (viaEffect.ServiceorContext.Tag), theRchannel accumulates those requirements.Effect.runPromiseonly accepts effects whereR = never(all dependencies provided). You must provide all services viaLayerbefore running. - The
Etype parameter tracks typed errors — unlikePromise, where errors areunknown, Effect tracks exactly which errors can occur at the type level.Effect.tryPromisereturnsUnknownExceptionby default because it can’t know what the promise rejects with. You must map it to a typed error. - Services are resolved at runtime through the Layer system — even if types check out, if you provide a
Layerthat doesn’t include all transitive dependencies, the runtimeprovidecall throws.
Fix 1: Run Effects Correctly
npm install effectimport { Effect, Console } from 'effect';
// Effects are descriptions — they don't execute until you run them
const program = Effect.gen(function* () {
yield* Console.log('Hello from Effect!');
const value = yield* Effect.succeed(42);
yield* Console.log(`Got value: ${value}`);
return value;
});
// Run as a Promise
const result = await Effect.runPromise(program);
// Output: Hello from Effect!
// Output: Got value: 42
// result = 42
// Run synchronously (only for effects that don't use async operations)
const syncResult = Effect.runSync(Effect.succeed(42));
// Run and handle both success and failure
Effect.runPromiseExit(program).then(exit => {
if (exit._tag === 'Success') {
console.log('Success:', exit.value);
} else {
console.log('Failure:', exit.cause);
}
});
// Run as a Fiber (non-blocking, cancellable)
const fiber = Effect.runFork(program);Using Effect.gen (generator syntax — most readable):
import { Effect } from 'effect';
// Generator syntax — yield* unwraps Effect values
const fetchUser = (id: string) =>
Effect.gen(function* () {
const response = yield* Effect.tryPromise({
try: () => fetch(`/api/users/${id}`),
catch: () => new FetchError({ message: 'Network error' }),
});
if (!response.ok) {
return yield* Effect.fail(new NotFoundError({ id }));
}
const user = yield* Effect.tryPromise({
try: () => response.json() as Promise<User>,
catch: () => new ParseError({ message: 'Invalid JSON' }),
});
return user;
});
// Type: Effect<User, FetchError | NotFoundError | ParseError, never>Fix 2: Define and Provide Services
Services use Context.Tag for dependency injection:
import { Effect, Context, Layer } from 'effect';
// Step 1: Define the service interface and tag
class UserRepository extends Context.Tag('UserRepository')<
UserRepository,
{
readonly getById: (id: string) => Effect.Effect<User, NotFoundError>;
readonly getAll: () => Effect.Effect<User[]>;
readonly save: (user: User) => Effect.Effect<void>;
}
>() {}
// Step 2: Use the service in an effect
const getUserProfile = (id: string) =>
Effect.gen(function* () {
const repo = yield* UserRepository; // Request the service
const user = yield* repo.getById(id);
return { name: user.name, email: user.email };
});
// Type: Effect<UserProfile, NotFoundError, UserRepository>
// ^^^^^^^^^^^^^^ — requires UserRepository
// Step 3: Create a Layer that provides the service
const UserRepositoryLive = Layer.succeed(UserRepository, {
getById: (id) =>
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then(r => r.json()),
catch: () => new NotFoundError({ id }),
}),
getAll: () =>
Effect.tryPromise({
try: () => fetch('/api/users').then(r => r.json()),
catch: () => new Error('Failed to fetch users'),
}),
save: (user) =>
Effect.tryPromise({
try: () =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
}).then(() => undefined),
catch: () => new Error('Failed to save'),
}),
});
// Step 4: Provide the layer and run
const program = getUserProfile('123').pipe(
Effect.provide(UserRepositoryLive),
);
// Type: Effect<UserProfile, NotFoundError, never> — R is now never
const result = await Effect.runPromise(program);Fix 3: Compose Layers for Complex Dependency Trees
Real apps have services that depend on other services:
import { Effect, Context, Layer, Config } from 'effect';
// Database service
class Database extends Context.Tag('Database')<
Database,
{ readonly query: (sql: string) => Effect.Effect<any[]> }
>() {}
// Logger service
class Logger extends Context.Tag('Logger')<
Logger,
{ readonly info: (msg: string) => Effect.Effect<void> }
>() {}
// UserRepository depends on Database and Logger
const UserRepositoryLive = Layer.effect(
UserRepository,
Effect.gen(function* () {
const db = yield* Database;
const logger = yield* Logger;
return {
getById: (id: string) =>
Effect.gen(function* () {
yield* logger.info(`Fetching user ${id}`);
const rows = yield* db.query(`SELECT * FROM users WHERE id = '${id}'`);
if (rows.length === 0) return yield* Effect.fail(new NotFoundError({ id }));
return rows[0] as User;
}),
getAll: () => db.query('SELECT * FROM users') as any,
save: (user: User) =>
Effect.gen(function* () {
yield* db.query(`INSERT INTO users VALUES (...)`);
yield* logger.info(`Saved user ${user.name}`);
}),
};
})
);
// Provide dependencies for the layers
const DatabaseLive = Layer.succeed(Database, {
query: (sql) => Effect.tryPromise(() => /* pg pool query */ [] as any),
});
const LoggerLive = Layer.succeed(Logger, {
info: (msg) => Effect.log(msg),
});
// Compose layers — UserRepositoryLive needs Database and Logger
const AppLayer = UserRepositoryLive.pipe(
Layer.provide(Layer.merge(DatabaseLive, LoggerLive)),
);
// Run the full program
const program = getUserProfile('123').pipe(Effect.provide(AppLayer));
await Effect.runPromise(program);Fix 4: Typed Error Handling
Effect tracks errors at the type level:
import { Effect, Data } from 'effect';
// Define typed errors using Data.TaggedError
class NotFoundError extends Data.TaggedError('NotFoundError')<{
readonly id: string;
}> {}
class ValidationError extends Data.TaggedError('ValidationError')<{
readonly field: string;
readonly message: string;
}> {}
class NetworkError extends Data.TaggedError('NetworkError')<{
readonly url: string;
}> {}
// Effect that can fail with typed errors
const createUser = (input: unknown) =>
Effect.gen(function* () {
// Validate
if (!input || typeof input !== 'object') {
return yield* Effect.fail(new ValidationError({
field: 'body',
message: 'Invalid input',
}));
}
// Save
const response = yield* Effect.tryPromise({
try: () => fetch('/api/users', { method: 'POST', body: JSON.stringify(input) }),
catch: () => new NetworkError({ url: '/api/users' }),
});
if (response.status === 409) {
return yield* Effect.fail(new ValidationError({
field: 'email',
message: 'Email already exists',
}));
}
return yield* Effect.tryPromise({
try: () => response.json() as Promise<User>,
catch: () => new NetworkError({ url: '/api/users' }),
});
});
// Type: Effect<User, ValidationError | NetworkError, never>
// Handle specific errors
const handled = createUser(input).pipe(
// Catch and recover from specific error types
Effect.catchTag('ValidationError', (e) =>
Effect.succeed({ error: `Validation failed: ${e.field} — ${e.message}` }),
),
// Catch all remaining errors
Effect.catchTag('NetworkError', (e) =>
Effect.succeed({ error: `Network error: ${e.url}` }),
),
// Or catch all errors at once
// Effect.catchAll((e) => Effect.succeed({ error: String(e) })),
);
// Retry on transient errors
const withRetry = createUser(input).pipe(
Effect.retry({ times: 3 }), // Retry up to 3 times on any error
);Fix 5: Concurrency with Fibers
Effect provides structured concurrency through Fibers:
import { Effect, Fiber, Schedule, Duration } from 'effect';
// Run effects concurrently — collect all results
const fetchAll = Effect.all(
[fetchUser('1'), fetchUser('2'), fetchUser('3')],
{ concurrency: 3 }, // Max 3 concurrent
);
// Race — return the first to succeed
const fastest = Effect.race(
fetchFromPrimary(),
fetchFromFallback(),
);
// Fork a background fiber
const program = Effect.gen(function* () {
// Start background task
const fiber = yield* Effect.fork(
longRunningTask().pipe(
Effect.repeat(Schedule.spaced(Duration.seconds(30))),
),
);
// Do other work while fiber runs
yield* doMainWork();
// Wait for fiber or cancel it
yield* Fiber.interrupt(fiber);
});
// Timeout
const withTimeout = fetchUser('1').pipe(
Effect.timeout(Duration.seconds(5)),
// Returns Option<User> — None if timed out
);
// Scoped resources — auto-cleanup
const acquireConnection = Effect.acquireRelease(
Effect.sync(() => createDbConnection()), // Acquire
(conn) => Effect.sync(() => conn.close()), // Release (guaranteed)
);Fix 6: Schema Validation
@effect/schema provides runtime validation with full type inference:
npm install @effect/schemaimport { Schema } from '@effect/schema';
import { Effect } from 'effect';
// Define a schema
const UserSchema = Schema.Struct({
id: Schema.String,
name: Schema.String.pipe(Schema.nonEmptyString()),
email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
role: Schema.Literal('admin', 'user', 'viewer'),
createdAt: Schema.Date,
});
// Infer TypeScript type from schema
type User = Schema.Schema.Type<typeof UserSchema>;
// Decode unknown data — returns Effect with typed error
const parseUser = Schema.decodeUnknown(UserSchema);
// Type: (input: unknown) => Effect<User, ParseError, never>
const program = Effect.gen(function* () {
const raw = yield* Effect.tryPromise(() =>
fetch('/api/users/1').then(r => r.json())
);
// Validate and parse — fails with ParseError if invalid
const user = yield* parseUser(raw);
return user; // Fully typed User
});
// Encode back to JSON-safe format
const encodeUser = Schema.encodeUnknown(UserSchema);
// Transform between formats
const DateFromString = Schema.transform(
Schema.String,
Schema.Date,
{
decode: (s) => new Date(s),
encode: (d) => d.toISOString(),
},
);Fix 7: Runtime and Framework Integration
Effect runs on every modern JS runtime, but the integration surface differs.
Node.js (18+):
Node is the smoothest target. Use a recent LTS — Effect’s internal use of AbortSignal, structuredClone, and queueMicrotask is fine on 18+, but some experimental APIs (like the test runner with --experimental-vm-modules) interact poorly with Effect’s fiber scheduler. Stick to tsx or tsup for dev/build; ts-node is no longer recommended for Effect projects because it doesn’t preserve ESM semantics around dynamic imports.
# tsx is the recommended dev runner for Effect projects
npm install -D tsx
npx tsx src/index.tsFor TypeScript runner alternatives and edge cases, see Fix: tsx Not Working.
Bun:
Bun runs Effect natively without transpilation. Performance is generally better for fiber-heavy workloads thanks to Bun’s faster microtask queue. The one catch: Bun’s built-in fetch handles AbortSignal slightly differently than Node, so Effect.tryPromise wrapping fetch with a timeout may surface different cancellation behavior. If you need consistent behavior across Bun and Node, use Effect.timeout rather than AbortSignal directly. See Fix: Bun Not Working for Bun-specific issues.
Deno:
Effect works in Deno via npm:effect specifiers. The main issue is permission flags — Effect’s Config system reads from Deno.env, which requires --allow-env. If you forget, Config.string('DB_URL') silently fails because Deno throws a PermissionDenied error that Effect catches but doesn’t surface clearly:
deno run --allow-env --allow-net src/index.tsESM-only constraint:
The effect package is published as pure ESM. If your project still uses CommonJS ("type": "commonjs" in package.json or no type field), you get ERR_REQUIRE_ESM at runtime. Either set "type": "module" or use dynamic import() to load Effect inside async functions.
React integration via @effect-rx/rx-react:
Calling Effect.runPromise directly inside a useEffect works but skips Effect’s structured concurrency benefits and tends to leak fibers on unmount. The recommended pattern is @effect-rx/rx-react:
import { Result, useRxValue } from '@effect-rx/rx-react';
import { Rx } from '@effect-rx/rx';
const userRx = Rx.fn((id: string) =>
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then(r => r.json()),
catch: () => new NetworkError({ url: `/api/users/${id}` }),
})
);
function UserProfile({ id }: { id: string }) {
const result = useRxValue(userRx(id));
return Result.builder(result)
.onInitial(() => <p>Loading...</p>)
.onFailure(e => <p>Error: {e.toString()}</p>)
.onSuccess(user => <p>{user.name}</p>)
.render();
}This handles fiber cleanup on unmount, deduplicates concurrent requests, and integrates with React’s concurrent rendering. Without it, you’ll see “fiber not interrupted” warnings in dev and orphaned network requests in production.
Browser bundle size:
The full effect package weighs ~80KB gzipped. For frontend apps where every KB matters, prefer tree-shakeable imports (import { Effect } from 'effect') and avoid pulling in @effect/platform unless you genuinely need its file/HTTP abstractions. Modern bundlers tree-shake Effect well, but barrel re-exports from your own utility files can defeat that.
Service injection patterns per platform:
In a Node server, you typically build one root Layer at startup and Effect.provide it to every request handler. In serverless (Vercel, Lambda), the same approach causes cold-start latency because layers initialize per cold start. Cache the runtime with ManagedRuntime.make(AppLayer) and reuse it across invocations:
import { ManagedRuntime } from 'effect';
const runtime = ManagedRuntime.make(AppLayer);
export async function handler(event: any) {
return runtime.runPromise(processEvent(event));
}For TypeScript-specific issues around generic constraints inside Layer composition, see Fix: TypeScript Generic Constraint Error.
Still Not Working?
Type 'Effect<A, E, SomeService>' is not assignable to 'Effect<A, E, never>' — you have an unprovided service. Every service in the R channel must be satisfied via Effect.provide(layer) before Effect.runPromise. Use Effect.provide(Layer.merge(layerA, layerB)) to provide multiple services at once. Check that your layer composition includes all transitive dependencies.
yield* does nothing / returns the effect itself — you must be inside Effect.gen(function* () { ... }) for yield* to unwrap effects. Outside a generator, yield* is a syntax error or no-op. Also verify that every yield* targets an Effect value, not a plain promise (use Effect.tryPromise to wrap promises).
Effect runs but never completes — check for infinite loops in Effect.repeat without a Schedule termination condition. Also check for deadlocks: if two fibers wait on each other’s results via Fiber.join, neither can proceed. Use Effect.race or Effect.timeout to add upper bounds.
Config values are undefined at runtime — Config.string('DB_URL') reads from environment variables by default. The variable must exist when the effect runs. For testing, provide a ConfigProvider: Effect.provide(Layer.setConfigProvider(ConfigProvider.fromMap(new Map([['DB_URL', 'postgres://...']])))).
ERR_REQUIRE_ESM when importing effect — your package.json doesn’t have "type": "module" and a CJS file is trying to require('effect'). Switch the project to ESM, use a dynamic import('effect'), or downgrade to a CJS-compatible Effect version if you can’t migrate. The mixed-module strategy gets messy fast — committing fully to ESM is the cleanest fix.
Schema validation passes but the parsed value is the wrong type — for related runtime validation patterns, see Fix: Zod Validation Not Working. Effect Schema and Zod share the same trap: a transformation step (Schema.transform, Zod’s .transform()) silently coerces unexpected inputs into shapes that satisfy the output schema but lose data.
React component re-renders on every fiber tick — you’re probably calling Effect.runPromise inside the render body instead of useEffect or @effect-rx/rx-react. Runs inside render trigger state updates that trigger re-renders that re-run the effect.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
Fix: Sharp Not Working — Installation Failing, Image Not Processing, or Build Errors on Deploy
How to fix Sharp image processing issues — native binary installation, resize and convert operations, Next.js image optimization, Docker setup, serverless deployment, and common platform errors.