Shard provides specialized classes for handling asynchronous operations:
AsyncValue<T> - Sealed class representing idle, loading, data, or error statesAsyncValue<T> is a sealed class that represents the state of an async operation:
sealed class AsyncValue<T> {
// States
AsyncIdle<T> // Not started yet (e.g. a CommandShard before execute)
AsyncLoading<T> // Operation in progress
AsyncData<T> // Completed with data
AsyncError<T> // Failed with error
}
AsyncIdle is the not-yet-started state. FutureShard and StreamShard never enter it (they begin loading); it exists so mutation-style shards like CommandShard can start idle.
final asyncValue = AsyncData<int>(42);
asyncValue.isIdle // false
asyncValue.isLoading // false
asyncValue.hasData // true
asyncValue.hasError // false
asyncValue.dataOrNull // 42 (AsyncLoading/AsyncError return previousData; AsyncIdle returns null)
asyncValue.errorOrNull // null
Use Dart's pattern matching for clean state handling. Because AsyncValue is sealed with four cases, an exhaustive switch must cover all of them:
// Exhaustive switch — all four cases now required:
final widget = switch (asyncValue) {
AsyncIdle() => const Text('Tap to load'),
AsyncLoading() => const CircularProgressIndicator(),
AsyncData(:final data) => Text('Value: $data'),
AsyncError(:final error) => Text('Error: $error'),
};
For most code, the when method (recommended) is more concise than a switch, and maybeWhen lets you handle only the cases you care about:
final text = asyncValue.when(
idle: () => 'Tap to load',
loading: (previousData) => previousData == null ? 'Loading…' : 'Refreshing…',
data: (value) => 'Value: $value',
error: (error, stackTrace, previousData) => 'Failed: $error',
);
final canSubmit = asyncValue.maybeWhen(
data: (_) => true,
orElse: () => false,
);
The loading and error callbacks receive previousData so you can render stale content while a refresh is in flight.
Use mapData to transform the success value while preserving the current variant, and whenData to read the success value only when one is present:
// Transform the success value, preserving the variant (idle stays idle, etc.):
final AsyncValue<String> label = userState.mapData((u) => u.name);
// Read the success value only when present, else null:
final int? nameLength = userState.whenData((u) => u.name.length);
mapData keeps the shape of AsyncLoading and AsyncError, mapping any previousData through the same transform.
AsyncValue.guard wraps a future in a try/catch and returns the outcome as an AsyncValue. Use it to populate a shard without writing your own error handling:
class UserShard extends Shard<AsyncValue<User>> {
UserShard(this.repo) : super(const AsyncIdle());
final UserRepository repo;
Future<void> load() async {
emit(AsyncLoading(previousData: state.dataOrNull));
emit(await AsyncValue.guard(() => repo.fetch()));
}
}
guard runs the future and captures the outcome as AsyncData on success or AsyncError (with the stack trace and an optional previousData) when it throws. It never throws, so the await always resolves to a valid state you can emit directly.
Both AsyncLoading and AsyncError can retain previous data:
// During refresh, previous data is preserved
AsyncLoading<User>(previousData: currentUser)
// On error, previous data is still accessible
AsyncError<User>(error, stackTrace, currentUser)
The AsyncError constructor takes the error first, then an optional stackTrace and optional previousData: AsyncError(error, [stackTrace, previousData]). Retaining previous data lets dataOrNull keep returning the last successful value while loading or after a failure, so the UI never has to flash empty.
To build UI for async state, use AsyncShardBuilder (see Widgets) or ShardBuilder with AsyncValue and pattern matching. For the shard types themselves, see FutureShard and StreamShard.
Next: Response Caching to cache API responses and reduce network calls with FutureShard.
See also: CommandShard for mutation-style actions that start in the idle state.