Custom errors with tag

Next problem: we get a generic UnknownException as error, which doesn't tell us much about what went wrong.

We cannot distinguish between errors caused by fetchRequest from errors on jsonResponse, since their errors have the same UnknownException type.

That's why in effect we usually define our own custom errors. The only "requirement" is making them Tagged Errors, which means having a _tag parameter used as discriminator.

In its simplest form an error is just an object with a _tag key with a string literal value:

const fetchError = {
  // 👇 String literal "FetchError" (`as const`), a type of `string` won't work!
  _tag: "FetchError" as const
};

The type of this error object can be represented with an interface:

interface FetchError {
  readonly _tag: "FetchError";
}

const fetchError: FetchError = { _tag: "FetchError" };

While this is the simplest way to understand tagged errors, it's not the recommended way of defining errors.

For now this works. In the next lessons we are going to learn about Data.TaggedError, which is a more powerful method to collect information about errors.

tryPromise with custom errors

The tryPromise function adds a generic UnknownException when no custom error is provided.

We can provide our own error by using the try/catch function overload of Effect.tryPromise:

In typescript is possible to give multiple signatures to the same function (Function overloading).

tryPromise can accept a single function (with a generic UnknownException) or an object with try/catch to specify a custom error.

import { Effect } from "effect";

interface FetchError {
  readonly _tag: "FetchError";
}

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

Now fetchRequest has our custom FetchError as error.

We can do the same with jsonResponse:

import { Effect } from "effect";

interface FetchError {
  readonly _tag: "FetchError";
}

interface JsonError {
  readonly _tag: "JsonError";
}

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

const jsonResponse = (response: Response) =>
  /// 👇 Effect<unknown, JsonError>
  Effect.tryPromise({
    try: () => response.json(),
    catch: (): JsonError => ({ _tag: "JsonError" }),
  });

The great thing about effects is that composing functions with different errors will automatically accumulate all the errors in the error type.

Now our final main function has a union of possible errors:

/// Effect<unknown, FetchError | JsonError>
const main = fetchRequest.pipe(
  Effect.flatMap(jsonResponse)
);
Effect Playground

FetchError | JsonError means that main can fail with FetchError or JsonError.

Notice how this type was inferred by composing a function that has FetchError and another with JsonError = FetchError | JsonError