The first benefit of effect is error handling.
However, Effect.promise
does not handle errors.
const main = Effect.flatMap(
fetchRequest,
jsonResponse
);
Effect.runPromise(main);
When you use Effect.promise
effect assumes that the operation can never fail. In practice main
works just like Promise<unknown>
but wrapped in Effect
.
If the function inside
Effect.promise
throws, then executing the effect will throw as well! 👇
> effect-getting-started-course@1.0.0 dev
> tsx src/index.ts
node:internal/process/promises:289
triggerUncaughtException(err, true /* fromPromise */);
^
SyntaxError: Unexpected token 'N', "Not Found" is not valid JSON
Since we know that fetch
can fail we need another function: Effect.tryPromise
.
import { Effect } from "effect";
/// 👇 Effect<Response, UnknownException>
const fetchRequest = Effect.tryPromise(
() => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);
Whereas Effect.promise
returned an Effect<Response>
, Effect.tryPromise
instead collects any error and returns Effect<Response, UnknownException>
.
Types for errors
What is UnknownException
? Why Effect
has 2 generic parameters now?
When you execute any plain typescript function you have no way of knowing what may go wrong unless you read the function implementation:
const request: Promise<Response> = fetch("https://pokeapi.co/api/v2/pokemon/garchomp/");
Simple example: how do you know if fetch
can fail? request
is Promise<Response>
, so no idea if something can go wrong, or how.
This problem becomes even worst when you compose multiple functions.
const main: Promise<unknown> =
fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
.then((response) => response.json());
Since both request
and json
may fail you are left with two choices:
- Read the implementation of all the functions and check for any
throw
, then wrap each function with try/catch
const main = async (): Promise<unknown> => {
let response;
try {
response = await fetch("https://pokeapi.co/api/v2/pokemon/garchomp/");
} catch (e) {
// Error with fetch: Do something here
return;
}
try {
return response.json();
} catch (e) {
// Error with json: Do something here
return;
}
};
- Wrap everything in a single
try
/catch
and report a generic "Some error happened"
const main = async (): Promise<unknown> => {
try {
const response = await fetch("https://pokeapi.co/api/v2/pokemon/garchomp/");
return response.json();
} catch (e) {
// Some error somewhere 💁🏼♂️
return;
}
};
Effect
solves this problem by providing the error directly in the type:
const fetchRequest: Effect<Response, UnknownException> = Effect.tryPromise(
() => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);
When we compose fetchRequest
and jsonResponse
we get a program main
that returns Effect<unknown, UnknownException>
:
unknown
is the return type of callingresponse.json()
when everything works as expectedUnknownException
is the error type when eitherfetchRequest
orjsonResponse
fails
/// Effect<Response, UnknownException>
const fetchRequest = Effect.tryPromise(
() => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);
/// Effect<unknown, UnknownException>
const jsonResponse = (response: Response) => Effect.tryPromise(
() => response.json()
);
/// Effect<unknown, UnknownException>
const main = Effect.flatMap(fetchRequest, jsonResponse);
We still cannot distinguish between errors in
fetchRequest
andjsonResponse
since both are typed asUnknownException
.Don't worry!
tryPromise
also allows to define custom errors, we are going to learn how in the following lessons.