In Riverpod 3, what is AsyncValue, and how do you render its loading, data and error states in a Flutter widget?
answer
- sealed, three subclasses
- switch needs no default
- AsyncData(:final value)
- when with three callbacks
- value is nullable, not throwing
basics
~10 sAsyncValue is Riverpod's sealed result type for async providers: AsyncData, AsyncLoading or AsyncError. A widget watches the provider and switches over the AsyncValue - exhaustively, with no default - or calls when(data:, error:, loading:).
solid answer
~40 sWatching a `FutureProvider`, `StreamProvider` or `AsyncNotifierProvider` returns an `AsyncValue<T>`, not a `T`. `AsyncValue` is a **sealed** class with three concrete subclasses - `AsyncData`, `AsyncLoading` and `AsyncError` - so a Dart 3 `switch` expression over it is exhaustive without a default: `AsyncData(:final value) => FeedList(value)`, `AsyncError(:final error) => ErrorView(error)`, `AsyncLoading() => spinner`. The older callback style, `feed.when(data: ..., error: ..., loading: ...)`, still works. For quick checks there are `value` (nullable), `hasValue`, `hasError` and `isLoading`. In Riverpod 3, `value` never throws: it returns the data, or the previous data during loading or error, or `null` - the old throwing `value` was removed and `valueOrNull` renamed to `value`. You never write `try`/`catch` or an `isLoading` flag yourself.
code
dart · 24 linesimport 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final newsFeedProvider = FutureProvider<List<Article>>((ref) async {
return ref.watch(newsApiProvider).fetchHeadlines();
});
class NewsFeedPage extends ConsumerWidget {
const NewsFeedPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final feed = ref.watch(newsFeedProvider);
return switch (feed) {
AsyncData(:final value) => ListView(
children: [
for (final article in value) ListTile(title: Text(article.title)),
],
),
AsyncError(:final error) => Center(child: Text('Could not load news: $error')),
AsyncLoading() => const Center(child: CircularProgressIndicator()),
};
}
}go deeper
Recall the three states and write the switch with AsyncData, AsyncError and AsyncLoading branches. Know that watching an async provider gives an AsyncValue.
Explain why sealing makes the switch exhaustive, compare switch with when, and describe what value, hasValue and isLoading report.
Standardise how screens render async state, including error presentation and logging from the stack trace, and remove FutureBuilder duplication.
Decide a house style - patterns versus when, shared loading and error widgets - so async screens look and behave consistently across teams.
## What AsyncValue is An **async provider** - `FutureProvider`, `StreamProvider`, `AsyncNotifierProvider`, `StreamNotifierProvider` - does not hand widgets a `Future` or `Stream`. It exposes an `AsyncValue<T>`: a snapshot of the operation's state that the widget can render synchronously on every build. For a news app, `ref.watch(newsFeedProvider)` returns `AsyncValue<List<Article>>`. `AsyncValue` is a **sealed class**. Its concrete subclasses are: | Subclass | Meaning | Key fields | |---|---|---| | `AsyncData<T>` | a value is available | `value` (non-null type `T`) | | `AsyncLoading<T>` | the operation is in progress | `progress` (optional, 0 to 1) | | `AsyncError<T>` | the operation failed | `error`, `stackTrace` | Because the hierarchy is sealed, the Dart analyzer knows these are the only possibilities. ## Rendering with switch The recommended Riverpod 3 style is a Dart 3 `switch` expression: ```dart final feed = ref.watch(newsFeedProvider); return switch (feed) { AsyncData(:final value) => ArticleList(articles: value), AsyncError(:final error) => FeedError(message: '$error'), AsyncLoading() => const Center(child: CircularProgressIndicator()), }; ``` - No `default` branch is needed; forgetting a case is a compile error. - `:final value` destructures the field into a local. - The `AsyncError` branch can also bind `:final stackTrace` for logging. The Riverpod tutorial also shows a property-pattern style - `AsyncValue(:final value?)`, then `AsyncValue(error: != null)`, then `AsyncValue()` - and warns that with that style the order matters: check value first, error second, loading last. ## Rendering with when Before patterns existed, the idiom was a method with three required callbacks: ```dart return feed.when( data: (articles) => ArticleList(articles: articles), error: (error, stackTrace) => FeedError(message: '$error'), loading: () => const Center(child: CircularProgressIndicator()), ); ``` `when` is still in Riverpod 3 and adds flags for multi-state situations such as refreshing. Related helpers: - `maybeWhen` / `whenOrNull` - handle only some cases. - `whenData(cb)` - transform the data and keep loading and error as they are, returning a new `AsyncValue`. ## Quick accessors 1. `value` - the data, or the previous data while loading or after an error, or `null` if there has never been any. 2. `hasValue`, `hasError`, `isLoading` - booleans that can be true at the same time. 3. `error`, `stackTrace` - nullable on the base type. 4. `requireValue` - the data, or throws; for code that is sure data exists. ## What changed in Riverpod 3 - `AsyncValue` became sealed, enabling exhaustive switches. - `valueOrNull` was renamed to `value`; the old `value`, which rethrew errors, was removed. - `AsyncLoading` gained an optional `progress`. ## Common mistakes - Treating `ref.watch(newsFeedProvider)` as the list itself and calling `.length` on it. - Writing a `FutureBuilder` around a provider's future, which re-creates what `AsyncValue` already gives you. - Adding a `default` case, which hides a missing branch from the compiler. ## Choosing between switch and when Both styles produce the same screen for the simple case, and interviewers mostly want to hear that you know why the pattern style is now preferred: - The `switch` is plain Dart: the compiler checks exhaustiveness, and each branch can destructure exactly the fields it needs. - `when` hides multi-state details behind flags, which is convenient but makes it easier to forget what a refresh or an error after data looks like. - A `switch` on the concrete subclasses and a `switch` on properties such as `AsyncValue(:final value?)` behave differently when old data is carried along; pick one style per codebase and document it.
- How is AsyncValue different from Flutter's AsyncSnapshot?Both describe an async result, but `AsyncValue` is produced and cached by the provider rather than by a builder widget, so every widget watching the provider shares one fetch. It is sealed, so switches over it are exhaustive, and it can carry previous data together with a loading or error state, which is what makes stale-while-refreshing screens simple.
- What does whenData do on an AsyncValue<List<Article>>?It maps only the data, for example `feed.whenData((a) => a.length)` gives an `AsyncValue<int>`. Loading and error pass through unchanged, and if the callback throws, the result becomes an `AsyncError`. It is handy for deriving a value while keeping the three-state shape for the widget.
saying these in an interview costs you the question
- ref.watch on a FutureProvider returns the Future to await in build
- A switch over AsyncValue needs a default branch to compile
- In Riverpod 3 value throws when the provider is in error
- You must wrap provider reads in try/catch to handle failures
- AsyncValue is an enum with three constants