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
:
Since
FetchError
is defined as aclass
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"),
})
);
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 👇