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.
Libraries
- resultx
- Support for doing something awesome.