Sparround

Result vs Either (fpdart)

Either comes from functional programming and is widespread in the Flutter community's "clean architecture" templates. Its most-used Dart implementation is the fpdart package.

The convention: `Left` is failure, `Right` is success. So Either<Failure, List<Order>> means "either a Failure or a list of orders".

Structurally Either and Result are the same idea: unite two cases in one type and force the caller to consider both. The differences are in the details:

  • Result is Flutter's official design pattern, lives in your own file (10-15 lines), needs no dependency, and is opened with Dart's own switch.
  • Either brings a package, and in exchange a rich composition API: map, mapLeft, flatMap, getOrElse, match, tryCatch. fpdart also brings related types such as Option, Task and TaskEither — the last meaning "an asynchronous operation that may fail".

The choice is not about technical superiority but about your team and your project.

dart
// ══════ RESULT (rəsmi pattern) ══════
abstract interface class OrderRepository {
  Future<Result<List<Order>>> fetchMine();
}

class OrderRepositoryRemote implements OrderRepository {
  @override
  Future<Result<List<Order>>> fetchMine() async {
    try {
      final dtos = await _api.getOrders();
      return Result.ok(dtos.map((d) => d.toDomain()).toList());
    } on SocketException {
      return const Result.error(NetworkFailure());
    }
  }
}

// Notifier-də:
state = switch (await _repository.fetchMine()) {
  Ok(:final value) => OrdersState.loaded(value),
  Error(:final error) => OrdersState.failed(error),
};


// ══════ EITHER (fpdart) ══════
import 'package:fpdart/fpdart.dart';

abstract interface class OrderRepository {
  // Left = Failure, Right = uğur. Sıra konvensiyadır.
  Future<Either<Failure, List<Order>>> fetchMine();
}

class OrderRepositoryRemote implements OrderRepository {
  @override
  Future<Either<Failure, List<Order>>> fetchMine() async {
    try {
      final dtos = await _api.getOrders();
      // `Either.of` Right (uğur) yaradır.
      return Either.of(dtos.map((d) => d.toDomain()).toList());
    } on SocketException {
      return Either.left(const NetworkFailure());
    }
  }
}

// Notifier-də, variant 1 — fpdart-ın `match`-i:
state = (await _repository.fetchMine()).match(
  (failure) => OrdersState.failed(failure),
  (orders) => OrdersState.loaded(orders),
);

// Notifier-də, variant 2 — Dart-ın öz pattern-ləri:
state = switch (await _repository.fetchMine()) {
  Left(value: final failure) => OrdersState.failed(failure),
  Right(value: final orders) => OrdersState.loaded(orders),
};


// ══════ EITHER-in əsl gücü: kompozisiya ══════
// Ardıcıl əməliyyatlar `flatMap` ilə zəncirlənir; ilk uğursuzluqda
// zəncir dayanır və `Left` dəyəri sona qədər daşınır.
Future<Either<Failure, Receipt>> checkout(Cart cart) async {
  final reserved = await _inventory.reserve(cart);

  return reserved.flatMap((reservation) {
    // Bu blok yalnız `reserved` Right olduqda işləyir.
    return _pricing
        .quote(reservation)
        .map((quote) => Receipt(reservation: reservation, quote: quote));
  });
}

// Müqayisə üçün: `mapLeft` xətanı çevirir, dəyərə toxunmur.
final Either<String, List<Order>> forLogging =
    (await _repository.fetchMine()).mapLeft((f) => f.runtimeType.toString());

The same repository in both approaches. Note that fpdart's `Either` can also be opened with Dart's patterns.

Criterion`Result` (official)`Either` (fpdart)
DependencyNone — 15 lines in your own fileThe `fpdart` package
Official positionA design pattern in the Flutter docsNot present in the official docs
Readability for a new team member`Ok` / `Error` — immediately obvious`Left` / `Right` — you have to know the convention
Composition (chaining)By hand: `switch` plus early `return``flatMap`, `map`, `mapLeft` — its strength
Error type`Exception` (in the official implementation)Any type: `Either<Failure, T>`
Additional typesNone`Option`, `Task`, `TaskEither` and more
Learning curvePractically noneFamiliarity with FP concepts (functors, monads) helps

Recommendation. For this branch's stack (Riverpod plus freezed), Result fits better, and the reasons are concrete:

1. It is the official pattern: documented in the Flutter docs, so the team debate stays short. 2. No dependency: a 15-line file. No package version, no breaking changes, no pub outdated question. 3. Little overlap with `AsyncValue`: Riverpod's AsyncValue already models loading/error/data, so Result is only needed for writes. Pulling in a package for that looks expensive. 4. `switch` is enough: Dart 3's patterns work on Left/Right too, so the core of fpdart's API (match, fold) is already in the language.

There are cases where Either is the right call:

  • The team is comfortable with FP and flatMap chains are everyday language.
  • Multi-step flows with sequential failures (validate → compute → write) — there flatMap is far shorter than a stack of switches.
  • fpdart is already in the project and types like Option are in use.

The worst choice is running both: one repository returns a Result, another an Either. Then every new file raises "which one here?" and conversion helpers start appearing.

A practical note on migration: moving from Either to Result (or the other way) is mechanical and should be done once. The simplest start is to count the boundaries: grep -rn "Either<" lib | wc -l. Under 20 and it is a day's work; at 200 it has to be gradual (repository by repository), with a converter function (Either<Failure,T> → Result<T>) kept for the transition.

Practice. Write the same repository method twice: once with Result, once with Either (adding fpdart to dev_dependencies just to try it is fine). Then write the notifier-side usage for both and decide for yourself.

Done means: both variants compile, and ARCHITECTURE.md carries a one-line decision: "this project uses X for error flow, because …".

📚 Sources and documentation

  • The fpdart packageofficialpub.dev

    `Either`, `Option`, `Task`, `TaskEither` and their API — `Either.of`, `Either.left`, `map`, `mapLeft`, `flatMap`, `match`.

  • Error handling with Result objectsofficialdocs.flutter.dev

    The official alternative — and why it needs no dependency.

  • Dart: patternsofficialdart.dev

    Object patterns like `Left(value: final f)` — they work with fpdart too.