dart_either
Author: Petrus Nguyễn Thái Học
Either monad for Dart & Flutter — a type-safe, lightweight library for error handling and railway-oriented programming.
- ✅ Monad comprehensions — both
sync(Either.binding) andasync(Either.futureBinding) versions. - ✅ Async
map/flatMap— hides the boilerplate of working withFuture<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 ☕
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.1.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:
/// 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 a [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
// ^
// )
/// Extract the value from [Either]
final value1 = right.getOrElse(() => -1); // 10
final value2 = right.getOrHandle((l) => -1); // 10
/// Chain computations
final flatMap = right.flatMap((a) => Either.right(a + 10)); // Either.Right(20)
/// 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.orNull(); // 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 |
// Left('Left value')
final left = Either<Object, String>.left('Left value');
// or: Left<Object, String>('Left value')
// Right(1)
final right = Either<Object, int>.right(1);
// or: Right<Object, int>(1)
// Left('Left value') — short-circuits on the first bind that returns Left
Either<Object, String>.binding((e) {
final String s = left.bind(e);
final int i = right.bind(e);
return '$s $i';
});
// Left(FormatException(...))
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 'package:http/http.dart' as http;
// ─── 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);
// ─── 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);
// ─── Either.fromNullable ─────────────────────────────────────────────────────
Either.fromNullable<int>(null); // Left(null)
Either.fromNullable<int>(1); // Right(1)
// ─── Either.futureBinding ────────────────────────────────────────────────────
String url1 = 'url1';
String url2 = 'url2';
Either.futureBinding<String, http.Response>((e) async {
final response = await Either.catchFutureError(
(e, s) => 'Get $url1: $e',
() async {
final uri = Uri.parse(url1);
return http.get(uri);
},
).bind(e);
final id = Either.catchError(
(e, s) => 'Parse $url1 body: $e',
() => jsonDecode(response.body)['id'] as String,
).bind(e);
return await Either.catchFutureError(
(e, s) => 'Get $url2: $e',
() async {
final uri = Uri.parse('$url2?id=$id');
return http.get(uri);
},
).bind(e);
});
// ─── 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>> result = Either.sequence(eithers);
// ─── 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:::...'))
// ─── Either.parSequenceN ─────────────────────────────────────────────────────
Future<Either<String, BuiltList<int>>> result = Either.parSequenceN(
functions: [
() async => fetchNumber(1),
() async => fetchNumber(2),
() async => fetchNumber(3),
],
maxConcurrent: 2,
);
// ─── Either.parTraverseN ─────────────────────────────────────────────────────
Future<Either<String, BuiltList<int>>> idsEither = Either.parTraverseN(
values: [1, 2, 3],
mapper: (id) => () async => fetchNumber(id),
maxConcurrent: 2,
);
1.3. Extension methods
| Extension | 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 |
// ─── 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);
// ─── 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)
// ─── 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 |
tapLeft |
Side-effect on Left |
tap |
Side-effect on Right |
map |
Transforms the Right value |
mapLeft |
Transforms the Left value |
flatMap |
Chains computations |
bimap |
Transforms both sides |
exists |
Tests the Right value with a predicate |
all |
Returns true for Left or if Right matches the predicate |
getOrElse |
Extracts Right or falls back to a default |
orNull |
Extracts Right or returns null |
getOrHandle |
Extracts Right or maps Left to a value |
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 |
3. Extensions on Future<Either<L, R>>
| Method | Description |
|---|---|
thenFlatMapEither |
Async flatMap on a Future<Either> |
thenMapEither |
Async map on a Future<Either> |
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) { ... }
Either<AsyncError, BuiltList<User>> result = 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.
Future<Either<AsyncError, dynamic>> httpGetAsEither(String uriString) =>
Either.futureBinding<AsyncError, dynamic>((e) async {
final uri =
Either.catchError(toAsyncError, () => Uri.parse(uriString)).bind(e);
final response = await Either.catchFutureError(
toAsyncError,
() => http.get(uri),
).bind(e);
e.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(e);
});
Either<AsyncError, BuiltList<User>> toUsers(List list) { ... }
Either<AsyncError, BuiltList<User>> result = await Either.futureBinding((e) async {
final dynamic json =
await httpGetAsEither('https://jsonplaceholder.typicode.com/users').bind(e);
final BuiltList<User> users = toUsers(json as List).bind(e);
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
Libraries
- dart_either
- Author: Petrus Nguyễn Thái Học.