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.

Libraries

resultx
Support for doing something awesome.