Either
is used in functional programming to handle errors. Either
is an alternative to try
/ catch
/ throw
. These are 2 different strategies to deal with errors in your app.
In this post we are going to learn:
- What is
Either
- How
Either
is defined - Example of
Either
andtry
/catch
- Comparison between
Either
andtry
/catch
- Advantages of using
Either
After reading this post you will have learned a new powerful strategy to work with errors in your app π―
try / catch / throw - Imperative error handling
In imperative languages the usual strategy for handling error is using try
/ catch
blocks.
The idea is simple: whenever you encounter some unexpected situation (missing data, wrong values, inconsistencies) you throw an Exception using the throw
keyword.
double divide(int a, int b) {
if (b == 0) {
throw FormatException("Cannot divide by 0");
}
return a / b;
}
throw
tells the app that some "error" happened. Now the app is at risk!
The default behavior at this point is for the app to crash: if no other part of the code "catches" this exception, the app will simply stop working, showing a blank screen or closing itself.
This is where "Error Handling" comes in. You use a try
/ catch
block to execute any code that may fail.
try {
final result = divide(val1, val2);
} catch (e) {
/// Do some "Error Handling" here π
}
This code is responsible to stop the exception from propagating and manually define a strategy for dealing with an error (showing message to the user, logging the issue).
Either - Functional programming error handling
In functional programming the approach is completely different: it's called Either
.
Either
is a type that can contain only 2 possible values, either one or the other (no both!):
- An error value (called
Left
by convention) - A successful return value (called
Right
by convention)
Either<String, double> divide(int a, int b) {
if (b == 0) {
return Left("Cannot divide by 0"); /// `Left` (Error βοΈ)
}
return Right(a / b); /// `Right` (Success βοΈ)
}
Left
is responsible to handle every error: in case of exceptions the function will return a Left
containing some value which represents the error.
Right
instead is the "normal" return value in case no error happens.
Either
defines a return value for both the success (Right
) and error (Left
) case, making explicit what can go right and what can go wrong
How can a function have different return values (Left
in some case, Right
in another)?
Generic types - Either in Object Oriented languages
In Object Oriented languages the Either
type is implemented by using inheritance.
The Either
type is defined as an abstract class
. This means that Either
cannot be instantiated by itself.
Either
also defines 2 generic type parameters:
L
: Type of the value contained insideLeft
(error type)R
: Type of the value contained insideRight
(success response type)
/// π `L` and `R` generic types
abstract class Either<L, R> {}
We then define 2 more classes that implement Either
:
Left
: Concrete class that represent an error. As such,Left
contains a value of typeL
Right
: Concrete class that represent a successful response. As such,Right
contains a value of typeR
abstract class Either<L, R> {}
class Right<L, R> implements Either<L, R> {
final R value; /// Success value βοΈ
const Right(this.value);
}
class Left<L, R> implements Either<L, R> {
final L value; /// Error value βοΈ
const Left(this.value);
}
Example - try / catch or Either
Let's look at a more detailed example to understand the difference between these 2 approaches.
Let's imagine we have the following requirements to implement:
Given a
username
, we need to check if it is valid and return it in such case. Ausername
is valid if 1οΈβ£ it is not already used, if 2οΈβ£ it is longer than 4 characters but shorter than 16, and if 3οΈβ£ it does not contain symbols.
Let's also imagine that we are given the isNotAlreadyUsed
function and hasSymbol
function by another member of the team:
/// Someone else took care of implementing these two π
bool isNotAlreadyUsed(String username) => ...
bool hasSymbol(String username) => ...
??? checkUsername(String username) => ... /// We are working here π
Imperative error handling
Below you can see this requirements implemented using throw + try / catch.
String checkUsername(String username) {
if (isNotAlreadyUsed(username)) {
throw FormatException("Username is already used");
}
if (username.length < 4 || username.length > 16) {
throw FormatException("Username must be between 4 and 16 characters");
}
if (hasSymbol(username)) {
throw FormatException("Username cannot contain a symbol");
}
return username;
}
Quite simple: you check every erroneous formatting and you throw an exception in such cases.
Wait! That's not all. This code is only responsible to throw
errors, it does not handle them. We also need to show the main
function to see error handling in action:
void main() {
try {
final validUsername = checkUsername(username);
} catch (e) {
print("There username is not valid");
}
}
Note: For this example, we simply print a message if something goes wrong. In a real app, you may want to define some custom exceptions and change your error handling strategy based on the type of exception.
Functional programming error handling
And now let's look at the same requirements implemented using functional programming and the Either
type.
Either<String, String> checkUsername(String username) {
if (isNotAlreadyUsed(username)) {
return Left("Username is already used");
}
if (username.length < 4 || username.length > 16) {
return Left("Username must be between 4 and 16 characters");
}
if (hasSymbol(username)) {
return Left("Username cannot contain a symbol");
}
return Right(username);
}
Actually, the code looks nearly exactly the same! 2 main changes:
- Change the return type to
Either<String, String>
. This makes explicit that this function may go wrong. - Change
throw
withLeft
, retuning a value also in case of errors.
We then need to handle the value of Either
when calling the function. We use the match
method:
void main() {
final usernameEither = checkUsername(username);
usernameEither.match(
(error) => print('$error'), /// Error βοΈ
(username) => print('Valid username: $username'), /// Success βοΈ
);
}
Note: Another way to implement the function using the full API of the Either
type would be the following:
Either<String, String> checkUsername(String username) =>
Either<String, String>.fromPredicate(
username,
isNotAlreadyUsed,
(_) => "Username is already used",
)
.flatMap(
(username) => Either<String, String>.fromPredicate(
username,
(username) => username.length < 4 || username.length > 16,
(_) => "Username must be between 4 and 16 characters",
),
)
.flatMap(
(username) => Either<String, String>.fromPredicate(
username,
hasSymbol,
(_) => "Username cannot contain a symbol",
),
);
This function uses the fromPredicate
and flatMap
methods.
Compare try/catch and Either
for error handling
We can notice some advantages and disadvantages with these 2 approaches:
try
/catch
is easy and familiar:try
/catch
looks just like anif
/else
statement. Therefore it is easy to explain and quite straightforward: do you expect an error? Just wrap everything at some point withtry
/catch
and you are sure that nothing bad will happen.try
/catch
requires less code: Since you do not need to manually handle all possible error cases,try
/catch
tends to require less code to achieve the same result compared toEither
Either
is more safe: Usingthrow
we risk to crash the app if we forget to handle the exception. Since there is no compile time check, this situation may lead to errors at runtime for our users.Either
instead never crashes the app. Instead,Either
requires you to handle the error at compile timeEither
has a more powerful API: By usingEither
you have access to a more extensive API to recover from errors, update the return value, chaining functions and moreEither
makes the error explicit: UsingEither
, the return type of the function itself declares that something may go wrong. Usingthrow
instead you are required to read the definition of the function to know that it may fail
Either and Errors
Either
is designed to handle all possible errors and exceptions.
There are cases in which we actually want some unrecoverable error to crash the app.
In fact, Either
deals with recoverable errors: errors that are somehow expected and that we want to handle.
When we encounter unrecoverable errors or particularly exceptional cases that must never happen, it is safer to throw
and crash the app.
A good analogy is the following^1:
"Nurse, if there's a patient in room 5, can you ask him to wait?"
Recoverable error: "Doctor, there is no patient in room 5." Unrecoverable error: "Doctor, there is no room 5!"
Now you know how the Either
type works and how it compares to try
/ catch
. This was an overview of what the Either
type has to offer. The Either
API is actually more powerful and it can help in many more cases.
We are going to learn more about it in future posts. You can subscribe to my newsletter here below to stay up to date with new posts and updates π
Thanks for reading.