EffectTalk
Back to Tour
core-conceptsBeginner

Solve Promise Problems with Effect

Understand how Effect solves the fundamental problems of native Promises, such as untyped errors, lack of dependency injection, and no built-in cancellation.

Guideline

Recognize that Effect is not just a "better Promise," but a fundamentally different construct designed to solve the core limitations of native Promises in TypeScript:

  1. Untyped Errors: Promises can reject with any value, forcing try/catch blocks and unsafe type checks.
  2. No Dependency Injection: Promises have no built-in way to declare or manage dependencies, leading to tightly coupled code.
  3. No Cancellation: Once a Promise starts, it cannot be cancelled from the outside.

Rationale

While async/await is great for simple cases, building large, robust applications with Promises reveals these critical gaps. Effect addresses each one directly:

  • Typed Errors: The E channel in Effect<A, E, R> forces you to handle specific, known error types, eliminating an entire class of runtime bugs.
  • Dependency Injection: The R channel provides a powerful, built-in system for declaring and providing dependencies (Layers), making your code modular and testable.
  • Cancellation (Interruption): Effect's structured concurrency and Fiber model provide robust, built-in cancellation. When an effect is interrupted, Effect guarantees that its cleanup logic (finalizers) will be run.

Understanding that Effect was built specifically to solve these problems is key to appreciating its design and power.


Good Example (The Effect Way)

This code is type-safe, testable, and cancellable. The signature Effect.Effect<User, DbError, HttpClient> tells us everything we need to know.

import { Effect, Data } from "effect";

interface DbErrorType {
  readonly _tag: "DbError";
  readonly message: string;
}

const DbError = Data.tagged&#x3C;DbErrorType>("DbError");

interface User {
  name: string;
}

class HttpClient extends Effect.Service&#x3C;HttpClient>()("HttpClient", {
  sync: () => ({
    findById: (id: number): Effect.Effect&#x3C;User, DbErrorType> =>
      Effect.try({
        try: () => ({ name: `User ${id}` }),
        catch: () => DbError({ message: "Failed to find user" }),
      }),
  }),
}) {}

const findUser = (id: number) =>
  Effect.gen(function* () {
    const client = yield* HttpClient;
    return yield* client.findById(id);
  });

// Demonstrate how Effect solves promise problems
const program = Effect.gen(function* () {
  yield* Effect.logInfo("=== Solving Promise Problems with Effect ===");

  // Problem 1: Proper error handling (no more try/catch hell)
  yield* Effect.logInfo("1. Demonstrating type-safe error handling:");

  const result1 = yield* findUser(123).pipe(
    Effect.catchAll((error) =>
      Effect.gen(function* () {
        yield* Effect.logInfo(`Handled error: ${error.message}`);
        return { name: "Default User" };
      })
    )
  );
  yield* Effect.logInfo(`Found user: ${result1.name}`);

  // Problem 2: Easy composition and chaining
  yield* Effect.logInfo("\n2. Demonstrating easy composition:");

  const composedOperation = Effect.gen(function* () {
    const user1 = yield* findUser(1);
    const user2 = yield* findUser(2);
    yield* Effect.logInfo(`Composed result: ${user1.name} and ${user2.name}`);
    return [user1, user2];
  });

  yield* composedOperation;

  // Problem 3: Resource management and cleanup
  yield* Effect.logInfo("\n3. Demonstrating resource management:");

  const resourceOperation = Effect.gen(function* () {
    yield* Effect.logInfo("Acquiring resource...");
    const resource = "database-connection";

    yield* Effect.addFinalizer(() => Effect.logInfo("Cleaning up resource..."));

    const user = yield* findUser(456);
    yield* Effect.logInfo(`Used resource to get: ${user.name}`);

    return user;
  }).pipe(Effect.scoped);

  yield* resourceOperation;

  yield* Effect.logInfo("\n✅ All operations completed successfully!");
});

Effect.runPromise(Effect.provide(program, HttpClient.Default));

Anti-Pattern (The Promise Way)

This Promise-based function has several hidden problems that Effect solves:

  • What happens if db.findUser rejects? The error is untyped (any).
  • Where does db come from? It's a hidden dependency, making this function hard to test.
  • If the operation is slow, how do we cancel it? We can't.
// ❌ This function has hidden dependencies and untyped errors.
async function findUserUnsafely(id: number): Promise&#x3C;any> {
  try {
    const user = await db.findUser(id); // `db` is a hidden global or import
    return user;
  } catch (error) {
    // `error` is of type `any`. We don't know what it is.
    // We might log it and re-throw, but we can't handle it safely.
    throw error;
  }
}