Skip to content

Fix: Effect (effect-ts) Not Working — Effect Not Running, Services Missing, or Type Errors

FixDevs ·

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 runs

Or 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: UserService

Or 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 valuesEffect.log('Hello') creates an effect description. Nothing happens until you pass it to Effect.runPromise, Effect.runSync, or Effect.runFork. This is the most fundamental difference from imperative code.
  • The R type parameter tracks service dependencies — when your effect uses a service (via Effect.Service or Context.Tag), the R channel accumulates those requirements. Effect.runPromise only accepts effects where R = never (all dependencies provided). You must provide all services via Layer before running.
  • The E type parameter tracks typed errors — unlike Promise, where errors are unknown, Effect tracks exactly which errors can occur at the type level. Effect.tryPromise returns UnknownException by 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 Layer that doesn’t include all transitive dependencies, the runtime provide call throws.

Fix 1: Run Effects Correctly

npm install effect
import { 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/schema
import { 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 runtimeConfig.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles