resultx 1.0.4 copy "resultx: ^1.0.4" to clipboard
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


Pub Points Pub Version Monthly Downloads


🚀 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 await at 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);
// ✅ 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.

2
likes
140
points
158
downloads

Publisher

verified publisherfahid.dev

Weekly Downloads

A Future Aware Result class to grcefully handle success and error and their Futures without the need to await on each step

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

More

Packages that depend on resultx