resultx 1.0.4
resultx: ^1.0.4 copied to clipboard
A Future Aware Result class to grcefully handle success and error and their Futures without the need to await on each step
resultX
Say goodbye to try-catch hell and null-checking nightmares!
A Future-aware Result class that gracefully handles success and error states with elegant functional programming patterns. Chain operations without worrying about exceptions or null values.
Explore the docs »
Report Bug
·
Request Feature
🚀 Why resultX? #
The Problem: Traditional Dart error handling is messy and error-prone:
// ❌ Traditional approach - prone to exceptions and null errors
Future<UserProfile> getUserProfile(String userId) async {
try {
final response = await http.get('/api/users/$userId');
if (response.statusCode != 200) {
throw Exception('Failed to fetch user');
}
final userData = json.decode(response.body);
final preferences = await getUserPreferences(userData['id']); // Another API call
final avatar = await downloadAvatar(userData['avatarUrl']); // Yet another API call
return UserProfile(
name: userData['name'],
email: userData['email'],
preferences: preferences,
avatar: avatar,
);
} catch (e) {
// Which operation failed? Hard to tell!
throw Exception('Something went wrong: $e');
}
}
The Solution: Clean, chainable, type-safe error handling:
// ✅ With resultX - elegant and safe
Future<Result<UserProfile, String>> getUserProfile(String userId) async {
return fetchUser(userId)
.mapOnSuccess((user) => fetchUserPreferences(user.id))
.mapOnSuccess((prefs) => downloadAvatar(user.avatarUrl))
.mapSuccess((avatar) => UserProfile(
name: user.name,
email: user.email,
preferences: prefs,
avatar: avatar,
));
}
📦 resultX #
A Dart Future-aware, functional programming Result package inspired by Kotlin's Result class and functional programming's Either type.
✨ Key Features #
- 🔗 Future-Aware Chaining - Chain operations without
awaitat every step - 🛡️ Type-Safe - Full compile-time type safety with zero runtime surprises
- 🚀 Functional Programming - Elegant FP patterns instead of imperative try-catch blocks
- ⛓️ Method Chaining - Fluent API that reads like natural language
- 🎯 Multiple Result Extraction -
getOrThrow(),getOrNull(),flat()tuple destructuring - 🔄 Flexible Mapping - Transform success and error values with ease
- 💡 Zero Dependencies - Lightweight and fast
📥 Installation #
Add this to your package's pubspec.yaml file:
dependencies:
resultx: ^1.0.4 # Use the latest version
Then run:
dart pub get
🎯 Real-World Examples #
Example 1: API Data Processing Pipeline #
Problem: You need to fetch user data, validate it, process it, and save it. Any step can fail, and you want clear error handling.
import 'package:resultx/resultx.dart';
// API response might fail
Future<Result<Map<String, dynamic>, String>> fetchUserData(String userId) async {
try {
final response = await http.get(Uri.parse('/api/users/$userId'));
if (response.statusCode == 200) {
return Success(json.decode(response.body));
}
return Error('API returned ${response.statusCode}');
} catch (e) {
return Error('Network error: $e');
}
}
// Validation might fail
Result<User, String> validateUser(Map<String, dynamic> data) {
if (!data.containsKey('email') || !data['email'].contains('@')) {
return Error('Invalid email address');
}
if (!data.containsKey('name') || data['name'].isEmpty) {
return Error('Name is required');
}
return Success(User(data['name'], data['email']));
}
// Database save might fail
Future<Result<User, String>> saveUser(User user) async {
try {
await database.insert('users', user.toMap());
return Success(user);
} catch (e) {
return Error('Failed to save user: $e');
}
}
// 🎉 Beautiful chaining - no try-catch mess!
Future<void> processUser(String userId) async {
final result = await fetchUserData(userId)
.mapOnSuccess(validateUser)
.mapOnSuccess(saveUser)
.onSuccess((user) => print('✅ Successfully processed: ${user.name}'))
.onError((error) => print('❌ Failed: $error'));
// Handle the final result
result.when(
success: (user) => showSuccessMessage('User ${user.name} created!'),
error: (error) => showErrorDialog(error),
);
}
Example 2: Configuration Loading with Fallbacks #
Problem: Load app configuration from multiple sources with fallbacks, where each source might not exist or be malformed.
// File might not exist
Future<Result<String, String>> readConfigFile(String path) async {
try {
final file = File(path);
if (!await file.exists()) {
return Error('Config file not found at $path');
}
return Success(await file.readAsString());
} catch (e) {
return Error('Failed to read file: $e');
}
}
// JSON might be invalid
Result<Map<String, dynamic>, String> parseConfig(String jsonString) {
try {
return Success(json.decode(jsonString));
} catch (e) {
return Error('Invalid JSON: $e');
}
}
// Environment variables might be missing
Result<AppConfig, String> buildConfig(Map<String, dynamic> configData) {
final dbUrl = configData['database_url'] ?? Platform.environment['DATABASE_URL'];
final apiKey = configData['api_key'] ?? Platform.environment['API_KEY'];
if (dbUrl == null) return Error('Database URL not configured');
if (apiKey == null) return Error('API key not configured');
return Success(AppConfig(
databaseUrl: dbUrl,
apiKey: apiKey,
debug: configData['debug'] ?? false,
));
}
// 🚀 Try multiple config sources with graceful fallbacks
Future<AppConfig> loadConfiguration() async {
// Try production config first, then staging, then development
final configResult = await readConfigFile('config/prod.json')
.mapOnError((_) => readConfigFile('config/staging.json'))
.mapOnError((_) => readConfigFile('config/dev.json'))
.mapOnSuccess(parseConfig)
.mapOnSuccess(buildConfig);
return configResult.resolve(
onError: (error) {
print('⚠️ Using default config due to: $error');
return AppConfig.defaults(); // Fallback to defaults
}
).data;
}
📘 Core Concepts #
Creating Results #
Create Result<SuccessType, ErrorType> instances:
// Direct creation
Result<String, String> parseUserId(String input) {
if (input.isEmpty) {
return Error('User ID cannot be empty');
}
return Success(input.trim());
}
// Factory methods
final success = Result.success('Hello World');
final error = Result.error('Something went wrong');
// From throwing functions
final result = Result.handle(() => int.parse('42')); // Success(42)
final failed = Result.handle(() => int.parse('abc')); // Error(FormatException)
// From Futures
final futureResult = await Result.future(http.get(url));
Working with Future<Result> #
The magic happens with Future<Result<S, E>> - you can chain operations without await:
// ❌ Traditional async/await pyramid of doom
try {
final userResult = await fetchUser(userId);
final prefsResult = await fetchPreferences(userResult.id);
final settingsResult = await applySettings(prefsResult);
return settingsResult;
} catch (e) {
return handleError(e);
}
// ✅ Clean chaining with resultX
final result = await fetchUser(userId)
.mapOnSuccess((user) => fetchPreferences(user.id))
.mapOnSuccess((prefs) => applySettings(prefs))
.onError((error) => logError('User processing failed', error));
🛠️ API Reference #
Extracting Values #
.when() - Pattern Matching
Handle both success and error cases:
final message = await fetchUserProfile(userId).when(
success: (profile) => 'Welcome, ${profile.name}!',
error: (error) => 'Failed to load profile: $error',
);
.resolve() - Provide Fallback Values
Convert any Result into a Success by providing a fallback:
// Always returns the success value, never null
final profileData = await fetchUserProfile(userId)
.resolve(onError: (error) => UserProfile.guest()) // Fallback to guest profile
.data;
.getOrThrow() - Fail Fast
Get the value or throw an exception:
try {
final config = await loadConfiguration().getOrThrow();
app.configure(config);
} catch (e) {
logger.fatal('Failed to start app: $e');
exit(1);
}
.flat() - Tuple Destructuring
Extract both success and error as a Dart record:
final (profile, error) = await fetchUserProfile(userId).flat();
if (profile != null) {
displayProfile(profile);
} else {
showError('Profile loading failed: $error');
}
Transformations #
.mapSuccess() - Transform Success Values
// Transform a successful user into their display name
final displayName = await fetchUser(userId)
.mapSuccess((user) => '${user.firstName} ${user.lastName}')
.resolve(onError: (_) => 'Unknown User')
.data;
.mapOnSuccess() - Chain Operations That Can Fail
// Each operation can fail, but chaining is clean and safe
final enrichedUser = await fetchUser(userId)
.mapOnSuccess((user) => fetchUserPreferences(user.id))
.mapOnSuccess((prefs) => fetchUserPermissions(prefs.userId))
.mapSuccess((permissions) => EnrichedUser(user, prefs, permissions));
.onSuccess() & .onError() - Side Effects
Execute side effects without changing the result:
final result = await processPayment(paymentData)
.onSuccess((receipt) => analytics.trackPurchase(receipt))
.onSuccess((receipt) => print('✅ Payment successful: ${receipt.id}'))
.onError((error) => logger.error('💳 Payment failed: $error'))
.onError((error) => analytics.trackError('payment_failed', error));
Advanced Usage #
Making Results Nullable
// Convert Result<User, String> to Result<User?, String>
final optionalUser = await fetchUser(userId)
.nullable()
.resolve(onError: (_) => null) // Now we can use null as fallback
.data;
Fire-and-Forget Operations
// When you don't care about the result - useful for analytics, logging, etc.
await sendAnalyticsEvent(userId, 'page_view').execute();
await updateLastSeen(userId).execute();
🎯 Best Practices #
1. Use Descriptive Error Types #
// ❌ Generic strings
Result<User, String> fetchUser(String id);
// ✅ Specific error types
enum UserError { notFound, networkError, invalidId, unauthorized }
Result<User, UserError> fetchUser(String id);
2. Chain Related Operations #
// ✅ Clean pipeline
final result = await validateInput(data)
.mapOnSuccess(processData)
.mapOnSuccess(saveToDatabase)
.mapOnSuccess(sendNotification);
3. Handle Errors at the Right Level #
// ✅ Handle business logic errors early, technical errors at boundaries
final orderResult = await validateOrder(order)
.mapOnSuccess(calculateTax)
.mapOnSuccess(processPayment)
.onError((error) => logBusinessError(error)); // Log here
// Handle at UI boundary
orderResult.when(
success: (order) => showOrderSuccess(order),
error: (error) => showUserFriendlyError(error),
);
📄 License #
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
Happy Coding! 🎉
Stop wrestling with exceptions and start flowing with Results.