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 onjsonResponse
, since their errors have the sameUnknownException
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 genericUnknownException
) or an object withtry
/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)
);
FetchError | JsonError
means thatmain
can fail withFetchError
orJsonError
.Notice how this type was inferred by composing a function that has
FetchError
and another withJsonError
=FetchError | JsonError