fabrik_result
A Future<User> tells you nothing about what can go wrong. The caller finds
out at runtime, or reads the implementation, or wraps everything in
try/catch and hopes.
Future<Either<Failure, User>> getUser(String id);Now the signature is the documentation, and the compiler makes sure the failure case is handled.
fabrik_result is pure Dart with no dependencies — usable in a Flutter app, a
server, or a CLI.
Installation
Section titled “Installation”dependencies: fabrik_result: ^1.1.0import 'package:fabrik_result/fabrik_result.dart';Quick start
Section titled “Quick start”final result = await getUser('42');
result.fold( (failure) => showError(failure.message), (user) => navigateToProfile(user),);Because Either is a sealed class, you can also pattern match — and the
compiler will tell you if you miss a case, with no default branch needed:
final message = switch (result) { Left(value: final failure) => 'Failed: ${failure.message}', Right(value: final user) => 'Welcome, ${user.name}',};That exhaustiveness check is the main reason to prefer this over a hand-rolled result class.
Guides
Section titled “Guides”Capture exceptions at the boundary
Section titled “Capture exceptions at the boundary”Most code that can fail throws. tryCatch converts that into an Either in
one step, replacing the try/catch you would otherwise write in every
repository method:
Future<Either<Failure, User>> getUser(String id) { return Either.tryCatchAsync( () => api.fetchUser(id), (error, stackTrace) => Failure('$error'), );}There is a synchronous form for parsing and other non-async work:
final config = Either.tryCatch( () => jsonDecode(raw) as Map<String, dynamic>, (error, stackTrace) => ParseFailure('$error'),);The onError callback receives the stack trace as well, so it can be logged
before being discarded.
Transform without unwrapping
Section titled “Transform without unwrapping”map changes the success value and leaves a failure untouched. flatMap
chains another operation that can itself fail. A Left short-circuits the
whole chain:
final result = await getUser(id) .then((r) => r.map((user) => user.profile)) .then((r) => r.flatMap(validateProfile));Without these you end up writing fold calls that re-declare the failure
branch just to leave it alone:
// Verbose: both branches spelled out to change oner.fold((f) => left<Failure, int>(f), (v) => right<Failure, int>(v * 2));
// Same thingr.map((v) => v * 2);Read the value back
Section titled “Read the value back”final name = result.getOrElse((failure) => 'Anonymous');
result.onRight((user) => analytics.track('profile_loaded'));result.onLeft((failure) => logger.error(failure));
final user = result.rightOrNull; // null when it is a LeftModel absence with Option
Section titled “Model absence with Option”Option<T> makes “there might be no value” explicit, and interoperates with
Dart’s nullable types in both directions:
final name = Option.fromNullable(json['name'] as String?) .where((n) => n.isNotEmpty) .map((n) => n.trim()) .getOrElse(() => 'Anonymous');Turn a missing value into a typed failure with toEither:
Either<Failure, User> result = cachedUser.toEither(() => Failure('no cached user'));Return Unit when there is nothing to return
Section titled “Return Unit when there is nothing to return”Either<Failure, void> is not valid Dart. Unit is the stand-in:
Future<Either<Failure, Unit>> saveSettings(Settings settings) { return Either.tryCatchAsync( () async { await storage.write(settings); return unit; }, (error, stackTrace) => Failure('$error'), );}Comparison
Section titled “Comparison”try/catch | fabrik_result | dartz / fpdart | |
|---|---|---|---|
| Failure visible in the signature | no | yes | yes |
| Compiler-enforced handling | no | yes | partly |
Exhaustive switch | n/a | yes | no |
| Flutter dependency | n/a | none | none |
| API surface | n/a | small | large |
fabrik_result ships Either, Option and Unit with the combinators
people actually reach for. It deliberately stops before typeclasses, monad
transformers and the wider functional vocabulary — if you want those,
fpdart is the better fit.
API reference
Section titled “API reference”Either<L, R>
Section titled “Either<L, R>”Sealed, with subtypes Left<L, R> and Right<L, R>. Build values with the
left() and right() helpers.
| Member | Returns | Description |
|---|---|---|
fold(onLeft, onRight) | T | Collapse both sides to one value |
map(fn) | Either<L, T> | Transform the success value |
mapLeft(fn) | Either<T, R> | Transform the failure value |
flatMap(fn) | Either<L, T> | Chain another fallible step |
flatMapAsync(fn) | Future<Either<L, T>> | Asynchronous flatMap |
getOrElse(fn) | R | Success value, or a fallback |
swap() | Either<R, L> | Exchange the two sides |
isLeft / isRight | bool | Which side is present |
leftOrNull / rightOrNull | L? / R? | Nullable access |
onLeft(fn) / onRight(fn) | void | Side effects |
Statics
| Member | Returns |
|---|---|
Either.tryCatch(body, onError) | Either<L, R> |
Either.tryCatchAsync(body, onError) | Future<Either<L, R>> |
Option<T>
Section titled “Option<T>”Sealed, with subtypes Some<T> and None<T>. Build values with some() and
none().
| Member | Returns | Description |
|---|---|---|
Option.fromNullable(value) | Option<T> | None when null |
toNullable() | T? | Back to a nullable |
fold(onNone, onSome) | R | Collapse both cases |
map(fn) / flatMap(fn) | Option<R> | Transform or chain |
where(predicate) | Option<T> | Keep the value only if it matches |
getOrElse(fn) | T | Value, or a fallback |
toEither(onNone) | Either<L, T> | Absence becomes a typed failure |
isSome / isNone | bool | Which case is present |
A single-value type for “succeeded, nothing to return”. The instance is the
constant unit.