Separate program definition from error handling

You may have noticed that the .gen code is missing catchTags:

const main = Effect.gen(function* () {
  const response = yield* fetchRequest;
  if (!response.ok) {
    return yield* new FetchError();
  }

  return yield* jsonResponse(response);
});

It's good practice to separate the implementation of our program from error handling. In this way we can focus on the logic, and only later deal with the errors based on the type signature:

  • program contains the core implementation (with type Effect<unknown, FetchError | JsonError>)
  • main derives from program after error handling (with type Effect<unknown, never>, notice the never type for errors)
/// 👇 Core implementation
const program = Effect.gen(function* () {
  const response = yield* fetchRequest;
  if (!response.ok) {
    return yield* new FetchError();
  }

  return yield* jsonResponse(response);
});

/// 👇 Error handling
const main = program.pipe(
  Effect.catchTags({
    FetchError: () => Effect.succeed("Fetch error"),
    JsonError: () => Effect.succeed("Json error"),
  })
);

The most common and suggested way of doing error handling is to use the pipe operator after program.


At this point we have the following code:

index.ts
import { Data, Effect } from "effect";

/** Errors **/
class FetchError extends Data.TaggedError("FetchError")<{}> {}
class JsonError extends Data.TaggedError("JsonError")<{}> {}


/** Implementation **/
const fetchRequest = Effect.tryPromise({
  try: () => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/"),
  catch: () => new FetchError(),
});

const jsonResponse = (response: Response) =>
  Effect.tryPromise({
    try: () => response.json(),
    catch: () => new JsonError(),
  });

const program = Effect.gen(function* () {
  const response = yield* fetchRequest;
  if (!response.ok) {
    return yield* new FetchError();
  }

  return yield* jsonResponse(response);
});


/** Error handling **/
const main = program.pipe(
  Effect.catchTags({
    FetchError: () => Effect.succeed("Fetch error"),
    JsonError: () => Effect.succeed("Json error"),
  })
);


/** Running effect **/
Effect.runPromise(main).then(console.log);
Effect Playground

With this we are able to implement our logic using linear code inside .gen, track all errors, and then separately handle them. Everything is type-safe, since all errors are reported on the types of Effect.

Wait, are we forgetting something?

The return type of main is still Effect<unknown>. Why unknown? We are looking for a Pokémon here!

How do we make sure that unknown is actually a valid Pokémon?

Turns out this problem is common and not easy to solve with plain typescript.

Let's jump to the next module to learn how to fix this with Schema.