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.