One way is to delay fold
as long as possible to avoid unnecessary nesting.
Algebraic effects can be composed without needing to know, if a value is existent or an operation has failed.
For example, a TaskEither
transformed with chain
will keep the first error, if no user has been found. Otherwise, it contains either the fetchUser
error or the User
data in success case.
Working example
import { pipeable as P, option as O, taskEither as TE, nonEmptyArray as NA } from "fp-ts";
type User = { name: string };
// global state (side effect)
let user: string | undefined = undefined;
const setUser = (usr: string) => (user = usr);
// given input
const userId: O.Option<string> = O.some("Arun");
const fetchUser: (uid: string) => TE.TaskEither<NA.NonEmptyArray<string>, User> = uid =>
TE.taskEither.of({ name: "Arun" });
// An error case would be: TE.left(NA.of("Unable to fetch user"))
const res = P.pipe(
userId,
TE.fromOption(() => NA.of("No user found")),
TE.chain(fetchUser),
TE.map(user => JSON.stringify(user, null, 2)),
TE.fold( // we fold at the very end before applying the effect
err => TE.taskEither.fromIO(() => { setUser(err[0]); }),
user => TE.taskEither.fromIO(() => { setUser(JSON.stringify(user, null, 2)); })
),
TE.chain(() => TE.taskEither.fromIO(() => { console.log(user); }))
);
// run the effect
res();
PS: I assumed here, your fetchUser
is an async operation, which creates TaskEither
. You can switch it back to Either
, if needed.