← Articles

Using Either for error handling in Flutter

By Ann Tech · 6 March 2026

Dart's Either type (from the fpdart or dartz package) represents a value that can be one of two types — conventionally Left for failure and Right for success. It's an alternative to throwing exceptions for expected error paths, making error handling explicit in the type system.

Why Either instead of exceptions?

Exceptions are invisible in function signatures. A function that throws NetworkException or NotFoundException looks identical to one that never throws — both return Future<Order>. The caller has no compile-time reminder to handle errors.

// Throws? Which exceptions? You have to read the implementation.
Future<Order> getOrder(String id);

// Explicit: caller knows a failure is possible
Future<Either<OrderError, Order>> getOrder(String id);

This is especially valuable at the repository/service layer boundary.

Setup with fpdart

dependencies:
  fpdart: ^1.1.0

Basic usage

import 'package:fpdart/fpdart.dart';

// Define your error types
sealed class OrderError {
  const OrderError();
}

final class OrderNotFound extends OrderError {
  const OrderNotFound(this.id);
  final String id;
}

final class OrderNetworkError extends OrderError {
  const OrderNetworkError(this.message);
  final String message;
}

// Return Either from your repository
Future<Either<OrderError, Order>> getOrder(String id) async {
  try {
    final order = await api.fetchOrder(id);
    return Right(order);       // Success
  } on NotFoundException {
    return Left(OrderNotFound(id));  // Known failure
  } on DioException catch (e) {
    return Left(OrderNetworkError(e.message ?? 'Network error'));
  }
}

Consuming Either

final result = await orderRepo.getOrder('order-1');

// Pattern match with fold
result.fold(
  (error) => switch (error) {
    OrderNotFound(:final id) => showDialog(message: 'Order $id not found'),
    OrderNetworkError(:final message) => showSnackbar(message),
  },
  (order) => displayOrder(order),
);

// Or use when-style matching
if (result.isRight()) {
  final order = result.getRight().toNullable()!;
  displayOrder(order);
} else {
  handleError(result.getLeft().toNullable()!);
}

// Map over the success value
final orderName = result.map((order) => order.customerName);
// orderName is Either<OrderError, String>

Chaining operations

Future<Either<OrderError, Invoice>> generateInvoice(String orderId) async {
  return (await getOrder(orderId))
      .flatMap((order) {
        if (!order.isComplete) {
          return Left(OrderNotComplete(orderId));
        }
        return Right(order);
      })
      .map((order) => Invoice.fromOrder(order));
}

TaskEither for async Either

fpdart has TaskEither — an Either wrapped in an async task:

TaskEither<OrderError, Order> getOrderTask(String id) {
  return TaskEither.tryCatch(
    () => api.fetchOrder(id),
    (error, _) => error is NotFoundException
        ? OrderNotFound(id)
        : OrderNetworkError(error.toString()),
  );
}

// Chain tasks
TaskEither<OrderError, Invoice> getInvoiceTask(String orderId) {
  return getOrderTask(orderId)
      .filterOrElse(
        (order) => order.isComplete,
        (order) => OrderNotComplete(order.id),
      )
      .map((order) => Invoice.fromOrder(order));
}

// Run it
final result = await getInvoiceTask('order-1').run();

In BLoC

Future<void> _onFetchOrder(
  FetchOrder event,
  Emitter<OrderState> emit,
) async {
  emit(const OrderLoading());

  final result = await orderRepo.getOrder(event.id);

  result.fold(
    (error) => emit(OrderError(
      message: switch (error) {
        OrderNotFound(:final id) => 'Order $id was not found',
        OrderNetworkError(:final message) => message,
        OrderNotComplete() => 'Order is not yet complete',
      },
    )),
    (order) => emit(OrderLoaded(order)),
  );
}

When NOT to use Either

Either adds ceremony. Use it when:

  • The error is expected and the caller should handle it
  • You have multiple error types with different handling
  • You're building a library or service layer

Use exceptions when:

  • The error is truly unexpected (bug, programming error)
  • You're deep in implementation code, not at an API boundary
  • The function is only called from a single place where the error type is obvious

Mixing Either and exceptions

A pragmatic approach: use Either at the repository/service boundary, throw exceptions for programming errors:

// Repository: Either for expected errors
Future<Either<OrderError, Order>> getOrder(String id) async { ... }

// Internal helper: exceptions for unexpected errors
Order _parseOrder(Map<String, dynamic> json) {
  // If this throws, it's a bug, not a handled case
  return Order.fromJson(json);
}

Common pitfalls

Using Either everywhere. Not every function needs Either. Private functions, pure utilities, and UI code don't need this formality. Apply it at boundaries.

Fat Left types. Don't use Either<Exception, T> — that's as vague as throwing exceptions. Use a sealed class hierarchy with specific error types.

Forgetting to handle Left. The point of Either is making error handling mandatory. If you use .getRight().toNullable()! everywhere, you've bypassed the safety guarantee.

Sign in to like, dislike, or report.

Using Either for error handling in Flutter — ANN Tech