Skip to main content

Result and ValueResult Types

vm_result provides two sealed container types to represent the outcome of operations: Result<T> for UI-driven state, and ValueResult<T> for branching business logic.


1. Result<T>​

Result<T> is a sealed class representing the lifecycle of an asynchronous operation. It can be in one of four states:

StateFactoryMeaning
InitialResult.initial()Idle state, before an action has begun.
LoadingResult.loading()The operation is actively in-flight.
DataResult.data(T value)The operation completed successfully with a value.
ErrorResult.error(Exception error)The operation failed with an exception.

Convenience Getters​

Result<T> exposes simple getters to safely inspect the current state without manual type checking:

final Result<String> result = ...;

result.isInitial; // true if state is Initial
result.isLoading; // true if state is Loading
result.hasError; // true if state is Error
result.hasValue; // true if state is Data

// Safe type casting: returns null if the state does not match
result.value; // returns String? (null if not Data)
result.errorValue; // returns Exception? (null if not Error)
result.errorAs<E>(); // returns E? (cast exception to E if matches, else null)

result.asData; // returns ResultData<String>?
result.asError; // returns ResultError<String>?
result.asLoading; // returns ResultLoading<String>?
result.asInitial; // returns ResultInitial<String>?

Pattern Matching (when & maybeWhen)​

Because Result<T> is a sealed class, you can pattern match all states safely. The compiler will warn you if you miss a case.

Using when (Requires handling all cases):​

Widget build(BuildContext context, Result<User> state) {
return state.when(
initial: () => const Text('Tap "Load" to start.'),
loading: () => const CircularProgressIndicator(),
data: (user) => Text('Hello, ${user.name}!'),
error: (exception) => Text('Error: ${exception.toString()}'),
);
}

Using maybeWhen (Allows specifying a fallback):​

Widget build(BuildContext context, Result<User> state) {
return state.maybeWhen(
loading: () => const SmallSpinner(),
orElse: () => const Text('Idle or loaded'),
);
}

2. ValueResult<T>​

While Result<T> is perfect for building UI screens, it is less suited for business logic decisions. For example, if a user logs in, you need to branch your logic: if successful, navigate to Home; if failed, show an error dialog.

ValueResult<T> is a two-state sealed container designed exactly for this:

StateFactoryMeaning
SuccessValueResult.success(T data)The operation was successful, carries the data.
FailureValueResult.failure(Exception error)The operation failed, carries the exception.

When to use ValueResult​

ValueResult is returned by the runWithValueResult guard. You handle the outcome using pattern matching:

Future<void> submitOrder() async {
final result = await runWithValueResult(() => repository.placeOrder());

result.when(
success: (order) {
// Transition UI to the success screen
navigator.pushReplacementNamed('/order-success', arguments: order);
},
failure: (exception) {
// Trigger a one-shot UI snackbar/effect
emitEffect(ShowErrorSnackbar(exception.toString()));
},
);
}

Convenience Getters​

final ValueResult<User> result = ...;

result.isSuccess; // true if success
result.isFailure; // true if failure

result.data; // returns T? (null if failure)
result.failure; // returns Exception? (null if success)
result.errorAs<E>(); // returns E? (cast failure to E if matches, else null)

3. Handling Custom Exceptions​

In many projects, you might define a custom exception hierarchy, such as:

class AppException implements Exception {
const AppException(this.message);
final String message;
}

class NetworkFailure extends AppException {
const NetworkFailure() : super('Connection error');
}

Since Result.error and ValueResult.failure accept the standard Exception class, custom exceptions are fully supported (as they extend Exception).

To easily extract and cast custom exceptions in your UI/logic without manually casting, use the errorAs<E>() helper method on both Result and ValueResult:

// Result
if (result.hasError) {
final appException = result.errorAs<AppException>();
if (appException != null) {
print(appException.message);
}
}

// ValueResult
result.when(
success: (data) => handleSuccess(data),
failure: (exception) {
// Cast explicitly using errorAs:
final appException = result.errorAs<AppException>();

// Or pattern match on the exception directly:
switch (exception) {
case NetworkFailure():
showToast('No internet connection');
default:
showToast(exception.toString());
}
},
);