dart_either 2.2.0 copy "dart_either: ^2.2.0" to clipboard
dart_either: ^2.2.0 copied to clipboard

Either monad for Dart language and Flutter framework. Type-safe error handling, railway oriented programming. Supports Monad comprehensions, async map, async flatMap.

dart_either #

Author: Petrus Nguyễn Thái Học

Dart CI pub version pub prerelease codecov License: MIT Style: lints Hits

Either monad for Dart & Flutter — a type-safe, lightweight library for error handling and railway-oriented programming.

  • Monad comprehensions — both sync (Either.binding) and async (Either.futureBinding) versions.
  • Async map / flatMap — hides the boilerplate of working with Future<Either<L, R>>.
  • Type-safe — an explicit, compiler-friendly alternative to nullable values and thrown exceptions.

Credits: Ported and adapted from Λrrow-kt.


Support the project #

If you find this library useful, consider buying me a coffee ☕

Buy Me A Coffee

Why dart_either? #

Difference from dartz and fpdart #

Many projects import entire FP libraries (dartz, fpdart, …) but only use Either. This library extracts and adapts just the Either class from Λrrow-kt, keeping things focused and lightweight.

Feature dart_either
Inspired by Λrrow-kt, Scala Cats
Documentation Fully documented — every method/function has doc comments and examples
Test coverage Fully tested
Completeness Most complete Either implementation available for Dart/Flutter
Monad comprehensions ✅ Both sync and async
Async map / flatMap thenMapEither, thenFlatMapEither
Bundle size Very lightweight and simple (compare to dartz)

Getting started #

Add the dependency to your pubspec.yaml:

dependencies:
  dart_either: ^2.2.0

Then run:

dart pub get

Documentation & Examples #

Resource Link
📖 API Documentation https://pub.dev/documentation/dart_either/latest/dart_either/
💡 Examples https://github.com/hoc081098/dart_either/tree/master/example/lib
🐦 Flutter Example https://github.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart

Either monad #

Either<L, R> represents one of two possible values:

  • Right(R) — the "desired" / success value (right-biased).
  • Left(L) — the "undesired" / error value.

Related implementations in other languages:

Why Either? (click to expand)

In day-to-day programming, it is fairly common to find ourselves writing functions that can fail. For instance, querying a service may result in a connection issue, or some unexpected JSON response.

To communicate these errors, it has become common practice to throw exceptions; however, exceptions are not tracked in any way, shape, or form by the compiler. To see what kind of exceptions (if any) a function may throw, we have to dig through the source code. Then, to handle these exceptions, we have to make sure we catch them at the call site. This all becomes even more unwieldy when we try to compose exception-throwing procedures.

// What exceptions can this throw? You have to dig through the source to find out.
double throwsSomeStuff(int i) => throw UnimplementedError();

// Same here — no way to know from the type signature alone.
String throwsOtherThings(double d) => throw UnimplementedError();

// And here too.
List<int> moreThrowing(String s) => throw UnimplementedError();

// Any of the three above can throw — good luck tracking which one failed!
List<int> magic(int i) => moreThrowing( throwsOtherThings( throwsSomeStuff(i) ) );

Assume we happily throw exceptions in our code. Looking at the types of the functions above, any could throw a number of exceptions — we do not know. When we compose, exceptions from any of the constituent functions can be thrown. Moreover, they may throw the same kind of exception (e.g., ArgumentError) and, thus, it gets tricky tracking exactly where an exception came from.

How then do we communicate an error? By making it explicit in the data type we return.

Either is used to short-circuit a computation upon the first error. By convention, the right side of an Either is used to hold successful values.

Because Either is right-biased, it is possible to define a Monad instance for it. Since we only ever want the computation to continue in the case of Right (as captured by the right-bias nature), we fix the left type parameter and leave the right one free. So, the map and flatMap methods are right-biased.

Example:

// 1) Creation
// Create an instance of [Right]
final right = Either<String, int>.right(10); // Either.Right(10)

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

// Map the right value to a [String]
final mapRight = right.map((a) => 'String: $a'); // Either.Right(String: 10)

// Map the left value to an [int]
final mapLeft = right.mapLeft((a) => a.length); // Either.Right(10)

// Return [Left] if the function throws an error, otherwise return [Right]
final catchError = Either.catchError(
  (e, s) => 'Error: $e',
  () => int.parse('invalid'),
);
// Either.Left(Error: FormatException: Invalid radix-10 number (at character 1)
// invalid
// ^
// )

// 2) Operations
// Extract the value from [Either]
final value1 = right.getOrDefault(-1); // 10
final value2 = right.getOrHandle((l) => -1); // 10

// Chain computations
final flatMap = right.flatMap((a) => Either.right(a + 10)); // Either.Right(20)
final combined = right.combine(
  Either<String, int>.right(5),
  combineLeft: (a, b) => '$a,$b',
  combineRight: (a, b) => a + b,
); // Either.Right(15)
final flattened = Either<String, Either<String, int>>.right(
  Either<String, int>.right(10),
).flatten(); // Either.Right(10)
final merged = Either<int, int>.right(10).merge(); // 10

// 3) Pattern matching
// Pattern matching
right.fold(
  ifLeft: (l) => print('Left value: $l'),
  ifRight: (r) => print('Right value: $r'),
); // Right value: 10

right.when(
  ifLeft: (l) => print('Left: $l'),
  ifRight: (r) => print('Right: $r'),
); // Prints Right: Either.Right(10)

// Or use Dart 3.0 switch expression syntax 🤘
print(
  switch (right) {
    Left() => 'Left: $right',
    Right() => 'Right: $right',
  },
); // Prints Right: Either.Right(10)

// Convert to nullable value
final nullableValue = right.getOrNull(); // 10
final leftValue = left.leftOrNull(); // 'none'
print(leftValue); // 'none'
print(nullableValue); // 10

API Reference #

Full API docs: https://pub.dev/documentation/dart_either/latest/dart_either/

1. Creation #

1.1. Factory constructors

Constructor Description
Either.left Creates a Left value
Either.right Creates a Right value
Either.binding Sync monad comprehension
Either.catchError Wraps a throwing expression
Left Direct Left constructor
Right Direct Right constructor
// 1) Create Left/Right
final left = Either<Object, String>.left('Left value');
// or: Left<Object, String>('Left value')

final right = Either<Object, int>.right(1);
// or: Right<Object, int>(1)

// 2) Sync monad comprehension (short-circuits on first Left)
Either<Object, String>.binding((effect) {
  final String s = left.bind(effect);
  final int i = right.bind(effect);
  return '$s $i';
});

// 3) Catch thrown exception into Left
Either.catchError(
  (e, s) => 'Error: $e',
  () => int.parse('invalid'),
);

1.2. Static methods

Method Description
Either.catchFutureError Wraps an async throwing expression
Either.catchStreamError Wraps a stream that may throw
Either.fromNullable Converts a nullable value
Either.futureBinding Async monad comprehension
Either.parSequenceN Parallel sequence with concurrency limit
Either.parTraverseN Parallel traverse with concurrency limit
Either.sequence Sequences a list of Eithers
Either.traverse Maps + sequences a list
import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:dart_either/dart_either.dart';
import 'package:http/http.dart' as http;

// 1) Either.catchFutureError
Future<Either<String, http.Response>> eitherFuture = Either.catchFutureError(
  (e, s) => 'Error: $e',
  () async {
    final uri = Uri.parse('https://pub.dev/packages/dart_either');
    return http.get(uri);
  },
);
(await eitherFuture).fold(ifLeft: print, ifRight: print);


// 2) Either.catchStreamError
Stream<int> genStream() async* {
  for (var i = 0; i < 5; i++) {
    yield i;
  }
  throw Exception('Fatal');
}
Stream<Either<String, int>> eitherStream = Either.catchStreamError(
  (e, s) => 'Error: $e',
  genStream(),
);
eitherStream.listen(print);


// 3) Either.fromNullable
Either.fromNullable<int>(null); // Left(null)
Either.fromNullable<int>(1);    // Right(1)


// 4) Either.futureBinding
String url1 = 'url1';
String url2 = 'url2';
Either.futureBinding<String, http.Response>((effect) async {
  final response = await Either.catchFutureError(
    (e, s) => 'Get $url1: $e',
    () async {
      final uri = Uri.parse(url1);
      return http.get(uri);
    },
  ).bind(effect);

  final id = Either.catchError(
    (e, s) => 'Parse $url1 body: $e',
    () => jsonDecode(response.body)['id'] as String,
  ).bind(effect);

  return await Either.catchFutureError(
    (e, s) => 'Get $url2: $e',
    () async {
      final uri = Uri.parse('$url2?id=$id');
      return http.get(uri);
    },
  ).bind(effect);
});


// 5) Either.sequence
List<Either<String, http.Response>> eithers = await Future.wait(
  [1, 2, 3, 4, 5].map((id) {
    final url = 'url?id=$id';
    return Either.catchFutureError(
      (e, s) => 'Get $url: $e',
      () async {
        final uri = Uri.parse(url);
        return http.get(uri);
      },
    );
  }),
);
Either<String, BuiltList<http.Response>> sequencedResponses = Either.sequence(
  eithers,
);


// 6) Either.traverse
Either<String, BuiltList<Uri>> urisEither = Either.traverse(
  ['url1', 'url2', '::invalid::'],
  (String uriString) => Either.catchError(
    (e, s) => 'Failed to parse $uriString: $e',
    () => Uri.parse(uriString),
  ),
); // Left(FormatException('Failed to parse ::invalid:::...'))


// 7) Either.parSequenceN
Future<Either<String, BuiltList<int>>> parallelSequence = Either.parSequenceN(
  functions: [
    () async => fetchNumber(1),
    () async => fetchNumber(2),
    () async => fetchNumber(3),
  ],
  maxConcurrent: 2,
);

// 8) Either.parTraverseN
Future<Either<String, BuiltList<int>>> parallelTraverse = Either.parTraverseN(
  values: [1, 2, 3],
  mapper: (id) => () async => fetchNumber(id),
  maxConcurrent: 2,
);

1.3. Binding capability and extension methods

API Description
Stream.toEitherStream Converts a stream, catching errors into Left
Future.toEitherFuture Converts a future, catching errors into Left
T.left Wraps any value as Left
T.right Wraps any value as Right
EitherEffect.bind Extracts Right or short-circuits its binding scope
Either.bind Binds an Either through an EitherEffect
EitherEffect.bindFuture Awaits and binds through an EitherEffect
Future<Either>.bind Awaits and binds an Either
EitherEffect.ensure Short-circuits when a condition is false
EitherEffect.ensureNotNull Extracts a non-null value or short-circuits
EitherEffect.raise Short-circuits without constructing a Left to bind
// 1) Stream.toEitherStream
Stream<int> genStream() async* {
  for (var i = 0; i < 5; i++) {
    yield i;
  }
  throw Exception('Fatal');
}
Stream<Either<String, int>> eitherStream =
    genStream().toEitherStream((e, s) => 'Error: $e');
eitherStream.listen(print);


// 2) Future.toEitherFuture
Future<Either<Object, int>> f1 =
    Future<int>.error('An error').toEitherFuture((e, s) => e);
Future<Either<Object, int>> f2 =
    Future<int>.value(1).toEitherFuture((e, s) => e);
await f1; // Left('An error')
await f2; // Right(1)


// 3) T.left / T.right
Either<int, String> left = 1.left<String>();
Either<String, int> right = 2.right<String>();

2. Operations #

Method Description
isLeft Returns true if this is a Left
isRight Returns true if this is a Right
fold Applies one of two functions based on variant
foldLeft Left fold with an initial value
swap Swaps Left and Right
onLeft Side-effect on Left
onRight Side-effect on Right
map Transforms the Right value
mapLeft Transforms the Left value
flatMap Chains computations
bimap Transforms both sides
combine Combines two Either values
isRightAnd Tests the Right value with a predicate
all Returns true for Left or if Right matches the predicate
getOrDefault Extracts Right or falls back to an eager default value
getOrNull Extracts Right or returns null
leftOrNull Extracts Left or returns null
getOrHandle Extracts Right or maps Left to a value
flatten Flattens nested Either
merge Extracts value when both sides have same type
findOrNull Finds Right matching a predicate
when Pattern-match returning the matched value
handleErrorWith Recovers from Left with a new Either
handleError Recovers from Left with a new Right value
redeem Maps both sides to the same type
redeemWith Maps both sides to a new Either
toFuture Converts to a Future
getOrThrow Extracts Right or throws the Left value
final ok = Either<String, int>.right(10);
final err = Either<String, int>.left('boom');

// Predicates
ok.isRightAnd((v) => v > 0); // true
err.all((_) => false); // true

// Side effects
ok.onRight(print); // prints 10
err.onLeft(print); // prints boom

// Transformations and composition
ok.map((v) => v + 1); // Right(11)
ok.combine(
  Either<String, int>.right(2),
  combineLeft: (a, b) => '$a,$b',
  combineRight: (a, b) => a + b,
); // Right(12)
Either<String, Either<String, int>>.right(ok).flatten(); // Right(10)

// Recovery
err.handleError((l) => l.length); // Right(4)
err.handleErrorWith((l) => Either<String, int>.right(l.length)); // Right(4)

// Extractions
ok.getOrDefault(0); // 10
err.getOrHandle((l) => l.length); // 4
ok.getOrNull(); // 10
err.leftOrNull(); // 'boom'
Either<int, int>.right(10).merge(); // 10

// Pattern matching
ok.fold(
  ifLeft: (l) => 'Left: $l',
  ifRight: (r) => 'Right: $r',
); // Right: 10

Deprecated aliases: tapLeft -> onLeft, tap -> onRight, orNull -> getOrNull, exists -> isRightAnd. getOrElse is deprecated. Prefer getOrDefault(<value>) for eager fallback, or getOrHandle((_) => <value>) for lazy fallback.


3. Extensions on Future<Either<L, R>> #

Method Description
thenFlatMapEither Async flatMap on a Future<Either>
thenMapEither Async map on a Future<Either>
// 1) Define a reusable async pipeline with thenFlatMapEither / thenMapEither
Future<Either<AsyncError, dynamic>> httpGetAsEither(String uriString) {
  Either<AsyncError, dynamic> toJson(http.Response response) =>
      response.statusCode >= 200 && response.statusCode < 300
          ? Either<AsyncError, dynamic>.catchError(
              toAsyncError,
              () => jsonDecode(response.body),
            )
          : AsyncError(
              HttpException(
                'statusCode=${response.statusCode}, body=${response.body}',
                uri: response.request?.url,
              ),
              StackTrace.current,
            ).left<dynamic>();

  Future<Either<AsyncError, http.Response>> httpGet(Uri uri) =>
      Either.catchFutureError(toAsyncError, () => http.get(uri));

  final uri =
      Future.value(Either.catchError(toAsyncError, () => Uri.parse(uriString)));

  return uri.thenFlatMapEither(httpGet).thenFlatMapEither<dynamic>(toJson);
}

Either<AsyncError, BuiltList<User>> toUsers(List list) { ... }

// 2) Build end-to-end flow
Either<AsyncError, BuiltList<User>> usersEither = await httpGetAsEither(
        'https://jsonplaceholder.typicode.com/users')
    .thenMapEither((dynamic json) => json as List)
    .thenFlatMapEither(toUsers);

4. Monad comprehensions #

Use Either.binding (sync) or Either.futureBinding (async) for do-notation style sequential computations that short-circuit on the first Left.

Their callback receives an EitherEffect<L>: a package-issued, opaque, scope-bound binding capability. Use it as effect.bind(either), either.bind(effect), eitherFuture.bind(effect), or effect.raise(value). Its construction and binding behavior are library-owned; assigning it to another variable only aliases the same scope. Each Either.binding or Either.futureBinding invocation owns an isolated scope, ordinary exceptions propagate unchanged, and the capability must not be stored or invoked after that scope settles.

Synchronous binding

The following complete example combines the main EitherEffect operations:

  • ensureNotNull extracts a required nullable value.
  • bind unwraps a Right or propagates an existing Left.
  • ensure checks a condition and short-circuits when it is false.
  • raise short-circuits with an available left value without constructing a Left solely to bind it.
Either<String, int> parseQuantity(String input) {
  final quantity = int.tryParse(input);
  return quantity == null
      ? Either.left('Quantity must be an integer')
      : Either.right(quantity);
}

Either<String, int> calculateOrderTotal({
  required String? quantityInput,
  required int unitPrice,
  required int availableStock,
}) =>
    Either.binding((effect) {
      // 1) Require the nullable input.
      final input = effect.ensureNotNull(
        quantityInput,
        () => 'Quantity is required',
      );

      // 2) Bind an Either, propagating its Left automatically.
      final quantity = effect.bind(parseQuantity(input));

      // 3) Enforce a value-level invariant.
      effect.ensure(quantity > 0, () => 'Quantity must be positive');

      // 4) Raise a domain error without constructing a Left to bind.
      if (quantity > availableStock) {
        effect.raise('Only $availableStock items are in stock');
      }

      // 5) A normal return becomes Right(total).
      return quantity * unitPrice;
    });

final successfulOrder = calculateOrderTotal(
  quantityInput: '3',
  unitPrice: 20,
  availableStock: 10,
); // Right(60)

final invalidQuantity = calculateOrderTotal(
  quantityInput: 'three',
  unitPrice: 20,
  availableStock: 10,
); // Left('Quantity must be an integer')

final insufficientStock = calculateOrderTotal(
  quantityInput: '12',
  unitPrice: 20,
  availableStock: 10,
); // Left('Only 10 items are in stock')

raise returns Never, so it also works naturally in expressions such as nullable ?? effect.raise('missing').

Asynchronous binding

Either.futureBinding uses the same effect operations while allowing awaited Future<Either<L, R>> values to participate in the computation:

Future<Either<AsyncError, dynamic>> httpGetAsEither(String uriString) =>
    Either.futureBinding<AsyncError, dynamic>((effect) async {
      final uri = Either.catchError(
        toAsyncError,
        () => Uri.parse(uriString),
      ).bind(effect);

      final response = await Either.catchFutureError(
        toAsyncError,
        () => http.get(uri),
      ).bind(effect);

      effect.ensure(
        response.statusCode >= 200 && response.statusCode < 300,
        () => AsyncError(
          HttpException(
            'statusCode=${response.statusCode}, body=${response.body}',
            uri: response.request?.url,
          ),
          StackTrace.current,
        ),
      );

      return Either<AsyncError, dynamic>.catchError(
        toAsyncError,
        () => jsonDecode(response.body),
      ).bind(effect);
    });

Either<AsyncError, BuiltList<User>> toUsers(List list) { ... }

Either<AsyncError, BuiltList<User>> usersEither = await Either.futureBinding(
  (effect) async {
    final dynamic json = await httpGetAsEither(
      'https://jsonplaceholder.typicode.com/users',
    ).bind(effect);
    final BuiltList<User> users = toUsers(json as List).bind(effect);
    return users;
  },
);

References #


Features and bugs #

Please file feature requests and bugs at the issue tracker.


License #

MIT License

Copyright (c) 2021-2026 Petrus Nguyễn Thái Học
22
likes
0
points
29.5k
downloads

Publisher

verified publisherhoc081098.dev

Weekly Downloads

Either monad for Dart language and Flutter framework. Type-safe error handling, railway oriented programming. Supports Monad comprehensions, async map, async flatMap.

Repository (GitHub)
View/report issues

Topics

#hoc081098 #functional-programming #fpdart #dartz #either

Funding

Consider supporting this project:

www.buymeacoffee.com

License

unknown (license)

Dependencies

built_collection, meta

More

Packages that depend on dart_either