Skip to content

fabrik_result

pub.dev

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.

dependencies:
fabrik_result: ^1.1.0
import 'package:fabrik_result/fabrik_result.dart';
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.

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.

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 one
r.fold((f) => left<Failure, int>(f), (v) => right<Failure, int>(v * 2));
// Same thing
r.map((v) => v * 2);
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 Left

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'),
);
}
try/catchfabrik_resultdartz / fpdart
Failure visible in the signaturenoyesyes
Compiler-enforced handlingnoyespartly
Exhaustive switchn/ayesno
Flutter dependencyn/anonenone
API surfacen/asmalllarge

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.

Sealed, with subtypes Left<L, R> and Right<L, R>. Build values with the left() and right() helpers.

MemberReturnsDescription
fold(onLeft, onRight)TCollapse 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)RSuccess value, or a fallback
swap()Either<R, L>Exchange the two sides
isLeft / isRightboolWhich side is present
leftOrNull / rightOrNullL? / R?Nullable access
onLeft(fn) / onRight(fn)voidSide effects

Statics

MemberReturns
Either.tryCatch(body, onError)Either<L, R>
Either.tryCatchAsync(body, onError)Future<Either<L, R>>

Sealed, with subtypes Some<T> and None<T>. Build values with some() and none().

MemberReturnsDescription
Option.fromNullable(value)Option<T>None when null
toNullable()T?Back to a nullable
fold(onNone, onSome)RCollapse both cases
map(fn) / flatMap(fn)Option<R>Transform or chain
where(predicate)Option<T>Keep the value only if it matches
getOrElse(fn)TValue, or a fallback
toEither(onNone)Either<L, T>Absence becomes a typed failure
isSome / isNoneboolWhich case is present

A single-value type for “succeeded, nothing to return”. The instance is the constant unit.