Resultex
A robust, type-safe error handling ecosystem for Dart and Flutter that implements the Result pattern for clean, predictable error handling without exceptions.
๐ฆ Overview
resultex brings a functional programming approach to error handling by wrapping your operational
outcomes in a type-safe container. Instead of throwing heavy exceptions and messing up your call
stack, every function returns a Result that is either a SuccessResult wrapping your value or a
FailureResult containing structured error details.
Why Resultex?
- ๐ก๏ธ Zero Try-Catch Boilerplate - Handle operational errors exactly where they matter.
- ๐ Type-Safe Destructuring - The Dart compiler ensures you handle both success and failure states.
- ๐งฉ Pattern Matching - Fully optimized for Dart 3+ exhaustive switch expressions.
- ๐ Composable Pipelines - Chain complex operations effortlessly with
mapandflatMap. - ๐งช Highly Testable - Predictable control flow, making Unit Testing a breeze without unexpected runtime crashes.
- ๐ Clean Architecture Approved - Follows SOLID principles, separating business logic outcomes from presentation layers.
๐ Installation
Prerequisites
- Dart SDK:
>=3.0.0 <4.0.0 - Flutter SDK:
>=3.10.0
Add resultex to your pubspec.yaml:
dependencies:
resultex: ^2.5.0
Core Concepts
- Successful Result
final result = Result.success(User(id: 1, name: "Hassan"));
// Contains a SuccessResult wrapping a Success<User> container
- Failure Result
final result = Result.failure(Failure(message: "Network request timeout"));
// Contains a FailureResult with messages, codes, and optional stackTraces
- Exhaustive Pattern Matching (Dart 3+)
switch (result) {
case SuccessResult<User>(success: Success(:final value)):
print('Welcome back, ${value.name}!');
case FailureResult<User>(failure: final fail):
print('Error occurred: ${fail.message}');
}
Reactive UI Validation (ResultTextController)
Stop writing custom stateful boilerplate or messy condition branches just to validate form fields.
ResultTextController extends Flutter's native TextEditingController with a built-in functional
error boundary.
// 1. Declare the controller with atomic business logic predicates
final emailController = ResultTextController<String>(
validator: (text) {
if (text.isEmpty) return Result.failure(Failure(message: 'Email cannot be empty'));
if (!text.contains('@')) return Result.failure(Failure(message: 'Invalid email format'));
return Result.success(text.trim());
},
);
// 2. Evaluate instantly in your UI thread without boilerplate
@override
Widget build(BuildContext context) {
return TextField(
controller: emailController,
decoration: InputDecoration(
// Dynamic inline error reading!
errorText: emailController.validatedResult.when(
onSuccess: (_) => null,
onFailure: (fail) => fail.message,
),
),
);
}
Functional Side-Effect Interception (.inspect)
Sometimes you need to intercept data midway through a functional stream for analytics, local
caching, or debugging without modifying the wrapped payload state. Use .inspectSuccess and
.inspectFailure to execute clean passive telemetry.
final profileResult = await
authRepository.getProfile
().inspectSuccess
(
(user) => firebaseAnalytics.logUserLogin(user.id))
.inspectFailure((fail) => appLogger.critical('Telemetry crash: ${fail.message}'))
.map((user) => user
.
toSummary
(
)
); // The user object travels downstream completely unaltered!
Fluent Pipeline Constraints (.ensure)
Enforce atomic operational rules instantly downstream using .ensure. If the predicate yields false,
it automatically short-circuits the pipeline into a structured FailureResult.
final Result<User> adultUserResult = await
authRepository.getProfile
().ensure
(
(user) => user.age >= 18,
(user) => Failure(message: "Access Forbidden: ${user.name} is underaged."),
);
Crash-Proof Reactive Flows (.toResultStream())
Raw asynchronous Streams (like WebSockets, Location trackers, or Firebase listeners) are highly
prone to unhandled runtime leaks that crash the UI stack. Safely encapsulate them into stable
error-boundary streams.
// Transforms Stream<T> natively into a stable Stream<Result<T>>
Stream<Result<LocationData>> safeCoordinates = locationService
.listenToCoordinates()
.toResultStream();
safeCoordinates.listen
(
(result) {
result.when(
onSuccess: (coords) => updateMapPin(coords),
onFailure: (fail) => showNetworkAlert(fail.message),
);
});
Zero-Data Expressive Closures (VoidResult)
In functional design, returning Result
VoidResult logoutUser() {
try {
secureStorage.clearAllTokens();
return VoidResult.success(); // Expressive, type-safe substitute for void/null
} catch (e) {
return VoidResult.failure(Failure(message: e.toString()));
}
}
Reactive UI Layer
State Management via ResultNotifier
A lightweight, lifecycle-aware alternative to heavy state management boilerplate. Track any
asynchronous operation safely inside your controllers or state classes.
final userNotifier = ResultNotifier<User>();
// Automatically handles loading state, catches unexpected errors, and updates the UI
userNotifier.track
(
repository
.
fetchUserProfile
(
)
);
Declarative Layouts via ResultBuilder
Eliminate bloated conditions inside your widget tree. Separate your UI cleanly into three
predictable layout paths.
@override
Widget build(BuildContext context) {
return ResultBuilder<User>(
notifier: _userNotifier,
onLoading: (context) => const CircularProgressIndicator(),
onFailure: (context, failure) => Text('Error: ${failure.message}'),
onSuccess: (context, user) => Text('Hello, ${user.name}'),
);
}
Advanced Functional Pipelines
Type-Safe Record Zipping (Dart 3+)
Resultex provides an elegant, compile-time type-safe ecosystem to consolidate completely dynamic and
heterogeneous Result instances concurrently using modern Dart 3 Records.
Instead of manual index casting (as User), you can wrap your independent operations inside a
record tuple and call .zip() directly. If any internal operation yields a FailureResult, the
entire pipeline short-circuits instantly, propagating that precise failure downstream.
Heterogeneous Zip Example (Up to 5 Elements)
import 'package:resultex/resultex.dart';
Future<void> loadDashboard() async {
// 1. Fire asynchronous requests or continuous operations
final Result<User> userRes = await authRepository.getProfile();
final Result<CryptoWallet> walletRes = await cryptoRepository.getBalance();
// 2. Zip disparate types seamlessly with 100% type safety
final Result<DashboardView> dashboardResult = (userRes, walletRes).zip(
(User user, CryptoWallet wallet) {
// Both parameters are strongly typed based on the input record tuple!
return DashboardView(user: user, wallet: wallet);
},
);
// 3. Evaluate and fold into your presentation tier
dashboardResult.fold(
onSuccess: (view) => renderDashboard(view),
onFailure: (failure) => showErrorSnackBar(failure.message),
);
}
Asynchronous Chaining (asyncMap & asyncFlatMap)
Chain multiple asynchronous dependencies sequentially without running into await callback hell.
// Streamline complex database/network pipelines elegantly
Future<Result<Orders>> ordersResult = repository.getUser(1) // Future<Result<User>>
.asyncMap((user) => user.id) // Future<Result<int>>
.asyncFlatMap((id) => orderRepository.getOrders(id)); // Future<Result<Orders>>
Adapting Errors downstream (mapFailure)
Transform internal exceptions into localized or presentation-friendly error messages before hitting
the UI.
final uiResult = apiResult.mapFailure(
(fail) => Failure(message: 'Localized Error: ${fail.message}'),
);
Resilient Operation Retries (withRetry)
Attach configurable recovery retries to any transient network dispatch with support for exponential
backoff.
final networkResult = await
Result.guardAsync
(
() => (() => http.get(uri)).withRetry(
const RetryOptions(maxAttempts: 3, delay: Duration(seconds: 2), backoffFactor: 2.0),
),
);
Advanced Features & Extensions
- UI Layer Clean Mapping (.when()) The package provides a tailored Flutter extension to directly map your Result states into widgets inside the build method without bloated conditions.
import 'package:resultex/core/utils/result_flutterx_extension.dart';
@override
Widget build(BuildContext context) {
return userResult.when(
onSuccess: (user) => Text('Hello, ${user.name}'),
onFailure: (failure) => Text('Error: ${failure.message}'),
);
}
- Concurrent Parallel Execution (ResultUtils.combineAll) When executing multiple operations simultaneously, ResultUtils.combineAll fires them in parallel. If any operation fails, it aggregates all intercepted errors into a unified MultiFailure contract instead of short-circuiting on the first error.
import 'package:resultex/core/utils/result_utils.dart';
final Result<List<dynamic>> dashboardResult = await
ResultUtils.combineAll
([repository.fetchUserProfile(), // Future<Result<User>>
repository.fetchNotifications(), // Future<Result<List<Notif>>>
repository.fetchCryptoWallet(), // Future<Result<Wallet>>
]);
dashboardResult.when(
onSuccess: (data) {
final user = data[0] as User;
final wallet = data[2] as CryptoWallet;
// Render your screen using safely casted data
},
onFailure: (failure) {
if (failure is MultiFailure) {
print('${failure.failures.length} operations failed concurrently.');
}
},
);
- Operational Recovery Path (.recover()) Intercept an operational Failure and route it through an alternative backup execution pipeline smoothly.
Result<User> userResult = await
authRepository.fetchRemoteUser
();
Result<User> finalizedResult = userResult.recover((failure) {
print('Remote fetch failed: ${failure.message}. Falling back to local cache...');
return Result.success(localDatabase.getCachedUser());
});
Fluid Usage Guide
Safe Execution Closures (guard / guardAsync)
Automatically intercept synchronous or asynchronous unexpected exceptions and encapsulate them into
safe Result variants.
// Synchronous parsing guard
final result = Result.guard(() => jsonDecode(rawJsonString));
// Asynchronous API call guard
final networkResult = await
Result.guardAsync
(
() => http.get(Uri.parse(url
)
)
);
Functional Chaining (map & flatMap)
Transform successful values or sequentially chain multiple business operations without nesting
blocks.
// Map values on success
final nameResult = result.map((user) => user.name); // Result<String>
// Chain dependencies flatly
final ordersResult = await
repository.getUser
(1)
.then((res) => res.flatMap((user) => orderRepository.getOrders(user.id)
)
);
Advanced ResultExecutor Architecture
Wrap execution scopes into monitored contexts complete with automated structured logging and error
tracking capabilities.
final executor = ResultExecutor(logger: AppLogger());
final result = await
executor.executeAsync
(
() async {
`final data = await api.fetchData();
`return processData(data);
},
context
:
fetchAndProcessDashboardData
);
Best Practices
โ DO
- Use ResultUtils.combineAll for maximizing parallel performance across independent network dispatches.
- Use the .when() extension inside Flutter layout building pipelines for pristine scannability.
- Provide meaningful context tags within ResultExecutor blocks to maintain bulletproof debug logs.
โ DON'T
- Don't force-extract values without evaluating state via pattern matching or explicit folds.
- Don't catch generic raw exceptions manually inside execution blocks managed by guard hooks.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details. Open Source development is respected; feel free to modify, distribute, and implement this package in both public repositories and enterprise closed-source commercial systems.
Made with โค๏ธ for clean, maintainable Dart & Flutter code architectures.
Libraries
- core/di/di_module
- core/di/flutter_error_handler_module
- core/di/get_it_config
- core/di/result_executor_module
- core/extensions/advance_async_result_extension
- core/extensions/future_result
- core/extensions/future_retry_extensions
- core/extensions/result_extensions
- core/extensions/result_failure_mapping_extension
- core/extensions/result_filterx_extension
- core/extensions/result_flutterx_extension
- core/extensions/result_stream_extension
- core/extensions/result_transformation
- core/extensions/result_zip_record
- core/extensions/unit_extension
- core/utils/result_collection
- core/utils/result_utils
- core/utils/use_case
- resultex
- A centralized error handling and operation execution library.