The Either
monad specifies the Either data type as well as several functions that operate on top of it. The Either
data type represents the result of a computation that may fail. This is how fp-ts
defines it [1]:
interface Left<E> {
readonly _tag: "Left";
readonly left: E;
}
interface Right<A> {
readonly _tag: "Right";
readonly right: A;
}
export type Either<E, A> = Left<E> | Right<A>;
As we can see, it is a simple tagged union with two distinct tags: Left
and Right
. Left represents the failure while right represents the success of a computation. A computation may fail for different reasons. In such cases, we can use another tagged union to represent the left type, e.g.:
// parse-int.ts
import type { either } from "fp-ts";
interface NaNError {
readonly _tag: "NaNError";
readonly value: number;
}
interface OutOfRangeError {
readonly _tag: "OutOfRangeError";
readonly value: number;
}
export type ParseIntError = NaNError | OutOfRangeError;
export type ParseIntResult = either.Either<ParseIntError, number>;
export type ParseInt = (x: string) => ParseIntResult;
export declare const parseInt: ParseInt;
In the above example, we just created a new type signature for the (not so?) well known parseInt
function. The function signature of the original parseInt
function is the following:
(string: string, radix?: number | undefined) => number
It returns NaN
in case it cannot parse the string as an integer. It returns Infinity
in case the parsed number is outside of the safe range, i.e. too big or too small [2]. The function signature does not indicate these edge cases because both NaN
and Infinity
are still typed as number
. They can only be distinguished from valid numbers at runtime using the isNaN
and isFinite
functions, respectively.
Let's be honest, how many times do we actually remember to handle such edge cases? parseInt
is not the only example of such functions. There are many that may fail but their type signature does not indicate that: JSON.parse
, new Date()
, new Array()
, BigInt
, etc.
In my opinion, encoding all possible outcomes of a computation in the type signature is generally better. Life is too short to read possibly nonexistent documentation. Also, there are more important things to keep in mind when our IDEs could help us make sure that these edge cases are handled. For example, by enabling the noImplicitReturns
compiler option:
import { ParseIntError } from "./parse-int.js";
// @ts-expect-error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
export const handleParseIntError = (err: ParseIntError): number => {
if (err._tag === "NaNError") {
return 0;
}
};
Or, by implementing and using an assertUnreachable
function.
Now that we know why Either
is useful, let's see how to actually use it.
Constructors
In the previous post, we learnt that every monad needs to be constructible, mappable, and chainable. The Either
monad has two constructors, one for each tag:
import { either } from "fp-ts";
export const success = either.right(42);
export const failure = either.left({ _tag: "NaNError", value: NaN });
Mapping
The Either
monad offers two map functions: map
and mapLeft
. Each maps the encapsulated value of the corresponding tag. We may map the right/left values to a new value of the same type, or a completely different one, e.g.: turning Either<Error, number>
into Either<Error, { count: number }>
or Either<string, number>
.
For the following code examples, assume we have implemented the parseInt
function. We can then use map
/mapLeft
to evolve the success and failure paths, respectively:
import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
const parseCount = flow(
parseInt,
either.map((count) => ({ count })),
either.mapLeft(({ value }) => `"${value}" could not be parsed as an integer`)
);
export const res = parseCount("42");
// Value of res is { _tag: "Right", right: 42 }
export const res2 = parseCount("abc");
// Value of res2 is { _tag: "Left", left: '"42" could not be parsed as an integer' }
// Type of both res and res2 is Either<string, { count: number }>
Without using the Either monad (assume parseInt
returns ParseIntError | number
instead), the above code may look like this:
import { ParseIntError } from "./parse-int.js";
declare const parseInt: (_: string) => ParseIntError | number;
const parseCount = (x: string) => {
const parsedInt = parseInt(x);
if (typeof parsedInt === "number") {
return { count: parsedInt };
} else {
return `"${parsedInt.value} could not be parsed as an integer`;
}
};
export const res = parseCount("42");
// Value of res is 42
export const res2 = parseCount("abc");
// Value of res2 is '"42" could not be parsed as an integer'
// Type of both res and res2 is number | string
This code follows more of an imperative programming paradigm. It requires us to:
- store the intermediate result in a variable,
- figure out how to name that variable,
- fork the logic based on the return type.
With the Either monad code, we only need to declare what the different results should map to.
Chaining
chain
is kind of similar to map
. The difference is that the function passed into chain
must return another Either
. We can think of it as performing a mapping that may fail.
import { either } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
const add = (x: string, y: string) =>
pipe(
parseInt(x),
either.chain(
(a) =>
pipe(
parseInt(y),
either.map((b) => a + b)
) // `Either<ParseIntError, number>` is returned by this pipe
)
);
export const res = add("42", "24");
// Value of res is { _tag: "Right", right: 66 }
export const res2 = add("abc", "24");
// Value of res2 is { _tag: "Left", left: { _tag: "NaNError", value: NaN } }
// Type of both res and res2 is Either<ParseIntError, number>
In the above code, we first parse "42"
as an integer. Then, we parse "24"
as an integer but only if the first call to parseInt
succeeds. Finally, if the second call to parseInt
succeeds, we add the results together. Should any of the parsing steps fail, the corresponding left result is returned.
Without using the Either monad, the above code may look like this:
import { ParseIntError } from "./parse-int.js";
declare const parseInt: (_: string) => ParseIntError | number;
const add = (x: string, y: string) => {
const parsedIntX = parseInt(x);
const parsedIntY = parseInt(y);
if (typeof parsedIntX === "number") {
if (typeof parsedIntY === "number") {
return parsedIntX + parsedIntY;
} else {
return parsedIntY;
}
} else {
return parsedIntX;
}
};
export const res = add("42", "24");
// Value of res is 66
export const res2 = add("abc", "24");
// Value of res2 is { _tag: "NaNError", value: NaN }
// Type of both res and res2 is number | ParseIntError
This code shares the same properties with the imperative code example in the Mapping section. On top of that, it is less readable and maintainable because of the nested if
statements. Imagine we added three more computations (not necessarily the same parseInt
steps) each of which could fail. The level of nesting would be three levels deeper making it more likely to introduce bugs. chain
allows us to flatten otherwise nested if statements like so:
import { either } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
// either.right represents an arbitrary computation that may fail in this example
pipe(
parseInt("42"),
either.chain((x) => either.right(x)),
either.chain((x) => either.right(x)),
either.chain((x) => either.right(x)),
either.chain((x) => either.right(x))
);
Other functions
Besides constructors, mappers, and chainers, monads may implement any additional interfaces. This section describes some of the other functions implemented by the Either
monad.
getOrElse
The map
and mapLeft
functions, as demonstrated in the Mapping section, produce an instance of an Either
. What if we wanted to extract its value? That's when we make use of getOrElse
, which forces us to define a fallback value in case of a failure.
import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
const myParseInt = flow(
parseInt,
either.getOrElse(() => 0)
);
export const res = myParseInt("42");
// Value of res is 42, not a Right
export const res2 = myParseInt("abc");
// Value of res2 is 0, not a Left
// Type of both res and res2 is number
We can use getOrElseW
if we want to widen - that's what the W at the end of the function name stands for - the right type of the Either:
import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
const myParseInt = flow(
parseInt,
either.getOrElseW(() => "Zero" as const)
);
export const res = myParseInt("42");
// Value of res is 42
export const res2 = myParseInt("abc");
// Value of res2 is "Zero"
// Type of both res and res2 is number | "Zero"
In the unlikely scenario that the failure is unrecoverable and we want our program to crash, we could also use getOrElseW
to throw an error:
import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
const throwError = (e: Error) => {
throw e;
};
const myParseInt = flow(
parseInt,
either.getOrElseW(flow(either.toError, throwError))
);
export const res = myParseInt("42");
// Value of res is 42
// Type of res is number
export const res2 = myParseInt("abc");
// res2 never gets assigned a value because an error is thrown
either.toError
takes any value and does a best effort conversion to an Error
object. throwError
takes an Error object and throws it. Functions that don't return anything and just throw, have a return type of never
. Hence, getOrElseW
in this example widens the type of number
by never
. However, number | never
is the same type as number
.
fold
fold
is a function that allows us to transform both tags of an Either at the same time. It expects two functions as the first set of arguments:
- A function that takes a value of the left type of the Either and returns a value of type
A
(it can be anything). - A function that takes a value of the right type of the Either and also returns a value of type
A
.
With an Either value provided as the second argument, fold
executes one of the two provided functions based on the tag and returns a value of type A
. It can be used to extract the encapsulated value of the Either as well as perform mappings at the same time:
import { either } from "fp-ts";
import { flow, pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
const throwError = (e: Error) => {
throw e;
};
export const res = pipe(
parseInt("42"),
either.fold(flow(either.toError, throwError), (count) => ({ count }))
);
// Type of res is { count: number }
// Functionally the same as writing the below code
// but using fewer steps (1 instead of 3):
export const res2 = pipe(
parseInt("42"),
either.mapLeft(either.toError),
either.map((count) => ({ count })),
either.getOrElseW(throwError)
);
It is still possible to throw an error using our throwError
function from the previous example because any value is assignable to the type of never
.
chainFirst
chainFirst
is similar to chain
. It takes a function that has to return an Either
. If that function returns a Right
value, its result is thrown away. In such case, the result of the previous computation is returned, i.e. the argument passed into the provided function. In the opposite case, the Left
value is returned. I tend to use it for logging intermediate values or setting breakpoints.
import { either } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
export const res = pipe(
parseInt("42"),
either.chainFirst((count) => {
console.log({ count });
return either.right(undefined);
}),
either.getOrElse(() => 0)
);
// Type of res is number
There's also a chainFirstW
alternative, which may be useful for fail-fast type of validations that keep widening the Left type with new failure types:
import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";
interface IncorrectAnswerError {
readonly _tag: "IncorrectAnswerError";
readonly value: number;
}
const myParseInt = flow(
parseInt,
either.chainFirstW((count) =>
count === 42
? either.right(undefined)
: either.left({
_tag: "IncorrectAnswerError",
value: count,
} as IncorrectAnswerError)
)
);
export const res = myParseInt("42");
// Value of res is { _tag: "Right", right: 42 }
export const res2 = myParseInt("abc");
// Value of res2 is { _tag: "Left", left: { _tag: "NaNError", value: NaN } }
export const res3 = myParseInt("41");
// Value of res3 is { _tag: "Left", left: { _tag: "IncorrectAnswerError", value: 41 } }
// Type of res, res2, res3 is the same: Either<ParseIntError | IncorrectAnswerError, number>
Assume we were to validate a form with multiple input fields and we wanted to present all the errors at once. We'd have to somehow collect multiple possible failures instead of returning on the first encountered one. In such cases, we should use either.getValidation
. There's already a great article written about it by the author of fp-ts
himself. I highly recommend checking it out.
Wrap-up
-
Either
represents the result of a computation that may fail.-
Left
stands for failure. -
Right
stands for success.
-
- We use:
-
map
: to transform the right value into a different value and this transformation cannot fail. -
mapLeft
: to transform the left value into a different value and this transformation cannot fail. -
getOrElse
: to extract the right value while setting a default value in case of aLeft
. -
getOrElseW
: to extract the right value while widening its type, or throwing an error on an unrecoverableLeft
. -
fold
: to extract a single value fromEither
while applying transformations to both the left and right values. -
chain
: to transform the right value into a different value and this transformation can fail. -
chainFirst
: to log intermediate values or set breakpoints for debugging. -
chainFirstW
: to perform one or more fail-fast type of validations. -
getValidation
: to perform validations that can be collected.
-
If I missed a function from the Either monad that you often use or find particularly interesting, or I didn't cover a use case for the functions described in this post, please let me know in the comments 🙏
In the next post of this series, we will look into the Option
monad.
Top comments (2)
This code does not compile as you show it to.
I get the error:
Hi @amite, thanks for reporting this issue. I forgot the parentheses around the destructuring operator. The following line:
should be:
I'm trying to test all code examples in these articles but sometimes I forget. I'll make sure to fix this in the post, too.