Sparround

AsyncValue: loading, error, data

AsyncValue<T> unifies the three states of async work in one type: AsyncLoading, AsyncError, AsyncData. In practice that removes the need to maintain isLoading, error and data fields by hand and to handle their impossible combinations (loading and error at once).

In Riverpod 3, AsyncValue became sealed. That works together with Dart 3 pattern matching: a switch is checked for exhaustiveness by the compiler. Also, valueOrNull was renamed to value.

dart
// 1) Pattern matching (rəsmi sənəddəki üslub):
final activity = ref.watch(activityProvider);

return switch (activity) {
  AsyncValue(:final value?) => Text(value.title),
  AsyncValue(:final error?) => Text('Xəta: $error'),
  _ => const CircularProgressIndicator(),
};

// 2) when — daha tanış forma:
return activity.when(
  data: (value) => Text(value.title),
  error: (error, stackTrace) => Text('Xəta: $error'),
  loading: () => const CircularProgressIndicator(),
);

// 3) Pull-to-refresh: refresh(provider.future) indikator üçün Future qaytarır.
RefreshIndicator(
  onRefresh: () => ref.refresh(activityProvider.future),
  child: ListView(children: [/* ... */]),
);

The pattern-matching style from the official docs, and the `when` alternative.

StateWhat to showTypical mistake
First load (`AsyncLoading`, no value)A skeleton or a spinnerShowing an empty screen
Refresh (a value exists, `isLoading` is true)The old content plus a small indicatorShowing a spinner and losing the content — the screen "jumps"
Error with no valueAn error message plus a retry buttonLeaving an endless spinner
Error with a previous valueThe old content plus a warning banner or snackbarReplacing the whole screen with an error

Automatic retry in Riverpod 3. Per the docs, when a provider throws during computation it is retried automatically — up to 10 times, with an exponential backoff from 200 ms to 6.4 seconds. However, Error instances (unrecoverable bugs) and ProviderException are not retried. The behaviour is configured with the retry parameter on a provider, on ProviderScope or on ProviderContainer; retry: (retryCount, error) => null disables it entirely.

Three details worth knowing for interviews when working with async state:

  • `AsyncValue.guard` — instead of writing try/catch, it converts a thrown error into AsyncError: state = await AsyncValue.guard(() => repo.save(todo));
  • `provider.future` — gives you an async provider's result as a Future; used as await container.read(provider.future) in tests and with RefreshIndicator in the UI.
  • Optimistic updates — write the change into state immediately, then send it to the server, rolling back on failure. The fastest-feeling approach for users, but the rollback logic must be written.

📚 Sources and documentation