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
Eitheris defined - Example of
Eitherandtry/catch - Comparison between
Eitherandtry/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
Leftby convention) - A successful return value (called
Rightby 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.
Eitherdefines 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,Leftcontains a value of typeLRight: Concrete class that represent a successful response. As such,Rightcontains 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. Ausernameis 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
throwwithLeft, 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/catchis easy and familiar:try/catchlooks just like anif/elsestatement. Therefore it is easy to explain and quite straightforward: do you expect an error? Just wrap everything at some point withtry/catchand you are sure that nothing bad will happen.try/catchrequires less code: Since you do not need to manually handle all possible error cases,try/catchtends to require less code to achieve the same result compared toEitherEitheris more safe: Usingthrowwe 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.Eitherinstead never crashes the app. Instead,Eitherrequires you to handle the error at compile timeEitherhas a more powerful API: By usingEitheryou have access to a more extensive API to recover from errors, update the return value, chaining functions and moreEithermakes the error explicit: UsingEither, the return type of the function itself declares that something may go wrong. Usingthrowinstead 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.
