Define errors with TaggedError

We defined custom errors by giving them a _tag property. We used the most simple strategy of a typescript interface:

interface FetchError {
  readonly _tag: "FetchError";
}

interface JsonError {
  readonly _tag: "JsonError";
}

In reality this is not the best way to define errors in effect. A simple object does not help with collecting the complete context around an error.

Defining errors is a common part of working with effect. Therefore it's no surprise that effect comes with a great built-in helper for defining tagged errors called Data.TaggedError:

  • The string parameter contains the value that will be used as _tag
  • The type parameter defines extra information that are required to initialize the error
import { Data } from "effect";

class FetchError extends Data.TaggedError("FetchError")<
  {
    customMessage: string;
  }
> {}

In this example FetchError will have "FetchError" as _tag and it contains an extra customMessage: string value.

We can create a new instance of FetchError like a normal class:

class FetchError extends Data.TaggedError("FetchError")<
  {
    customMessage: string;
  }
> {}

const error = new FetchError({ customMessage: "some error message" });

error by default contains more information compared to an interface:

Data.TaggedError collects more information about the error like cause and stack, as well as custom properties we defined like "customMessage"
Data.TaggedError collects more information about the error like cause and stack, as well as custom properties we defined like "customMessage"

Since FetchError is defined as a class we can use it both as a value (new FetchError) and as a type.

Refactoring errors to use TaggedError

I recommend using TaggedError to define errors in effect.

We can refactor our API request to use TaggedError instead of interface:

import { Effect, Data } from "effect";

interface FetchError { 
  readonly _tag: "FetchError"; 
} 

interface JsonError { 
  readonly _tag: "JsonError"; 
} 

class FetchError extends Data.TaggedError("FetchError")<{}> {} 

class JsonError extends Data.TaggedError("JsonError")<{}> {} 

We then create a new class instance of each error when implementing the request:

class FetchError extends Data.TaggedError("FetchError")<Readonly<{}>> {}

class JsonError extends Data.TaggedError("JsonError")<Readonly<{}>> {}

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

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

const main = fetchRequest.pipe(
  Effect.filterOrFail(
    (response) => response.ok,
    () => new FetchError()
  ),
  Effect.flatMap(jsonResponse),
  Effect.catchTags({
    FetchError: () => Effect.succeed("Fetch error"),
    JsonError: () => Effect.succeed("Json error"),
  })
);
Effect Playground

At this point our main became harder to read:

const main = fetchRequest.pipe(
  Effect.filterOrFail(
    (response) => response.ok,
    () => new FetchError()
  ),
  Effect.flatMap(jsonResponse),
  Effect.catchTags({
    FetchError: () => Effect.succeed("Fetch error"),
    JsonError: () => Effect.succeed("Json error"),
  })
);

Using pipe requires a completely different mental model from your usual linear typescript code line-by-line.

We don't like this. It would be awesome to have all the benefits of effect (like error handling and composition), with linear and readable code.

Guess what? We can using .gen!

Let's see how in the next module 👇