Functional Error Handling with Either and fpdart in Flutter: An Introduction

The Dart language offers primitives such as try, catch, and throw for handling errors and exceptions in our apps.

But as we have learned in the previous article, it's easy to write unsafe code if we rely on the built-in exception handling system:

  • we can only find out if a function can throw by reading its documentation and/or implementation
  • we can't leverage static analysis and the type system to handle errors explicitly

We can overcome some of these issues by applying some functional programming principles to error handling and writing safer and more robust code.

And in this article, we'll focus on error handling by learning about the Either type from the fpdart package.

Since there is much to cover, here will focus on the basics. And in the next article, we'll learn how to deal with errors and asynchronous code in real-world Flutter apps using TaskEither.

In the previous article about Flutter Exception Handling, we have already seen how to use the Result type to handle errors more explicitly. As we'll see, Either and Result are very similar, and the fpdart package offers additional APIs that help us go beyond the limitations of the Result type.

So let's get started with a brief overview of functional programming.

What is Functional Programming?

Functional programming (or FP) is a fascinating topic that promotes using pure functions, immutable data, and a declarative programming style, helping us write more clean and maintainable code.

This is in contrast to object-oriented programming (OOP), which relies on mutable state and an imperative programming style to describe the data and behavior of the objects in our system.

Since many modern languages support both functional and object-oriented paradigms, you can adopt one style or the other as you see fit in your code.

In fact, you may have already used FP in your Dart and Flutter code by:

  • passing a function as an argument to another function (such as a callback)
  • using the map, where, reduce functional operators on Iterable types such as lists and streams
  • working with generics and type inference

Other functional programming features such as pattern matching, destructuring, multiple return values, and higher-order types have already been discussed in the Dart language funnel.

And error handling is one area where FP can bring substantial benefits.

So let's dive in, starting with a small example. 👇

Example: Parsing a Number

If we want to parse a String containing a numerical value into a double, we can write code like this:

final value = double.parse('123.45'); // ok

However, what would happen if we tried running this?

final value = double.parse('not-a-number'); // throws exception

This code throws an exception at runtime.

But the signature of the parse function doesn't tell us this, and we have to read the documentation to find out:

/// Parse [source] as a double literal and return its value. /// Throws a [FormatException] if the [source] string is not valid static double parse(String source);

If we want to handle the FormatException, we can use a try/catch block:

try { final value = double.parse('not-a-number'); // handle success } on FormatException catch (e) { // handle error print(e); }

But on large codebases, it's hard to figure out which functions might throw and which don't.

Ideally, we want the signature of our functions to make it explicit that they can return an error.

The Either type

The Either type from the fpdart package lets us specify both the failure and success types as part of the function signature:

import 'package:fpdart/fpdart.dart'; Either<FormatException, double> parseNumber(String value) { try { return Either.right(double.parse(value)); } on FormatException catch (e) { return Either.left(e); } }

The values inside Either.left and Error.right must match the type annotations we have defined (FormatException and double in this case). Always use Either.left to represent errors, and Either.right to represent the return value (success).

Comparing Either and Result

At first sight, Either is very similar to the Result type that is available in the multiple_result package:

Result<FormatException, double> parseNumber(String value) { try { return Success(double.parse(value)); } on FormatException catch (e) { return Error(e); } }

In fact, only the basic syntax changes:

  • Either.rightSuccess
  • Either.leftError

But Either has a much more extensive and powerful API. Let's take a look. 👇

Either and the tryCatch factory constructor

If we want to simplify our implementation, we can use the tryCatch factory constructor:

Either<FormatException, double> parseNumber(String value) { return Either.tryCatch( () => double.parse(value), (e, _) => e as FormatException, ); }

This is how tryCatch is implemented:

/// Try to execute `run`. If no error occurs, then return [Right]. /// Otherwise return [Left] containing the result of `onError`. factory Either.tryCatch( R Function() run, L Function(Object o, StackTrace s) onError) { try { return Either.of(run()); } catch (e, s) { return Either.left(onError(e, s)); } }

Note that the onError callback gives the error and stack trace as arguments, and the error type is Object.

But since we know that the double.parse function can only ever throw a FormatException, it's safe to cast e as a FormatException in our parseNumber function.

Fun Example: Functional Fizz-Buzz 😎

Now that we've seen how to use Either<E, S> to represent errors in our code, let's take a step forward and use it to manipulate some data.

For example, here is a Dart function that implements the popular fizz buzz algorithm:

String fizzBuzz(double value) { if (value % 3 == 0 && value % 5 == 0) { // multiple of 3 and 5 return 'fizz buzz'; } else if (value % 3 == 0) { // multiple of 3 return 'fizz'; } else if (value % 5 == 0) { // multiple of 5 return 'buzz'; } else { // all other numbers return value.toString(); } }

And suppose that we want to implement a function defined as follows:

Iterable<Either<FormatException, String>> parseFizzBuzz(List<String> strings);

This function should return an Iterable list of values by:

  • parsing each of the strings in the input list with the parseNumber function
  • if the number is valid, apply the fizzBuzz function to it and return Either.right with the result
  • if the number is not valid, return Either.left with a FormatException

Fizz-buzz: Imperative style

As a first attempt, we could implement this with some imperative code:

Iterable<Either<FormatException, String>> parseFizzBuzz(List<String> strings) { // all types are declared explicitly for clarity, // but we could have used `final` instead: List<Either<FormatException, String>> results = []; for (String string in strings) { // first, parse the input string Either<FormatException, double> parsed = parseNumber(string); // then, use map to convert valid numbers using [fizzBuzz] Either<FormatException, String> result = parsed.map((value) => fizzBuzz(value)); // add the value results.add(result); } return results; }

The most interesting line is this:

Either<FormatException, String> result = parsed.map((value) => fizzBuzz(value));

Here we use the Either.map operator to convert all values (of type double) to results (of type String) by calling the fizzBuzz function.

If we check the documentation, we discover that Either.map is only applied if Either is Right, meaning that any errors are carried over automatically:

/// If the [Either] is [Right], then change its value from type `R` to /// type `C` using function `f`. Either<L, C> map<C>(C Function(R a) f);

Fizz-buzz: Functional style

Guess what?

We can implement the parseFizzBuzz function in a completely functional style:

Iterable<Either<FormatException, String>> parseFizzBuzz(List<String> strings) => strings.map( (string) => parseNumber(string).map( (value) => fizzBuzz(value) // no tear-off ) );

And we can make it even shorter using a tear-off:

Iterable<Either<FormatException, String>> parseFizzBuzz(List<String> strings) => strings.map( (string) => parseNumber(string).map( fizzBuzz // with tear-off ) );

Either way (pun intended 😅), we have replaced the imperative for loop by mapping over the list of strings.

And the best thing is that errors don't get in the way when we map values using Either.

Running the Fizz-Buzz implementation

As proof of this, let's run this code:

void main() { final values = [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', 'not-a-number', 'invalid', ]; // parse, then join by inserting a newline final results = parseFizzBuzz(values).join('\n'); print(results); }

And this is the output:

Right(1.0) Right(2.0) Right(fizz) Right(4.0) Right(buzz) Right(fizz) Right(7.0) Right(8.0) Right(fizz) Right(buzz) Right(11.0) Right(fizz) Right(13.0) Right(14.0) Right(fizz buzz) Left(FormatException: Invalid double not-a-number) Left(FormatException: Invalid double invalid)

Cool, isn't it? 😎

What else can we do with Either?

Want to create an instance of Either directly from a left or right value?

/// Create an instance of [Right] final right = Either<String, int>.of(10); /// Create an instance of [Left] final left = Either<String, int>.left('none');

How about mapping values?

/// Map the right value to a [String] final mapRight = right.map((a) => '$a'); /// Map the left value to a [int] final mapLeft = right.mapLeft((a) => a.length);

Fancy some pattern matching?

/// Pattern matching final number = parseNumber('invalid'); /// unwrap error/success with the fold method number.fold( // same as number.match() (exception) => print(exception), (value) => print(value), );

I won't list all the things you can do with Either here. Take a peek at the documentation to learn about the 40+ methods you can use to create and manipulate values.

And since all these methods are composable, we can create complex functions out of simple ones. To learn more about this, read this article about chaining functions by the author of fpdart.

The fpdart package is fully documented and you don't need any previous experience with functional programming to start using it. Check the official documentation for more details.

Conclusion

We've now ventured into the world of functional programming, by learning about Either and the fpdart package.

Here are the key points:

  • we can use Either<L, R> as an alternative to throwing exceptions whenever we want to declare errors explicitly in the signature of our functions/methods (leading to self-documenting code).
  • if we use Either and don't handle errors, our code won't compile. This is much better than discovering errors at runtime during development (or worse, in production 🥶).
  • Either comes with an extensive API, making it easy to manipulate our data with useful functional operators such as map, mapLeft, fold, and many others.

The examples I presented were academic on purpose, making it easier to cover the basics.

But in the next article, we'll learn how to deal with errors and asynchronous code in real-world Flutter apps using TaskEither.

And we'll see how to make the most of fpdart and TaskEither when we use a reference app architecture with presentation, application, domain, and data layers, which is well suited for medium and large-sized apps. 👍

New Flutter Course Now Available

I launched a brand new course that covers error handling in great depth, along with other important topics like state management with Riverpod, app architecture, testing, and much more:

Want More?

Invest in yourself with my high-quality Flutter courses.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Fully updated to Dart 2.15.

Flutter Animations Masterclass - Full Course

Flutter Animations Masterclass - Full Course

Master Flutter animations and build a completely custom habit tracking application.