Fix: Effect (effect-ts) Not Working — Effect Not Running, Services Missing, or Type 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:
- 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(),
},
);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://...']])))).
For related TypeScript issues, see Fix: TypeScript Generic Constraint Error and Fix: Zod Validation Not Working.
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.