skip to content

AsyncValue States

Async providers expose results as AsyncValue, a sealed type of data, loading and error that keeps previous data while refreshing. Interviewers probe how a screen renders each state.

part ofFlutter Riverpodoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Riverpod 3, what is AsyncValue, and how do you render its loading, data and error states in a Flutter widget?

level: juniorimportance: must knowfreq 64%

answer

  1. sealed, three subclasses
  2. switch needs no default
  3. AsyncData(:final value)
  4. when with three callbacks
  5. value is nullable, not throwing

basics

~10 s

AsyncValue 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 s

Watching 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 lines
dart
import '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

for a junior

Recall the three states and write the switch with AsyncData, AsyncError and AsyncLoading branches. Know that watching an async provider gives an AsyncValue.

for a middle

Explain why sealing makes the switch exhaustive, compare switch with when, and describe what value, hasValue and isLoading report.

for a senior

Standardise how screens render async state, including error presentation and logging from the stack trace, and remove FutureBuilder duplication.

for a principal

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
open as a page

In a Riverpod 3 AsyncNotifier, what does AsyncValue.guard do, and what does state hold if a guarded news-feed refresh fails?

level: middleimportance: should knowfreq 40%

basics

~20 s

AsyncValue.guard runs an async callback and returns AsyncData on success or AsyncError on failure, replacing try/catch. After a failed guarded refresh, state is an AsyncError that still carries the previously loaded articles as its value.

open as a page

In Riverpod 3, what does a news feed's AsyncValue contain while it refreshes, and how do isRefreshing and skipLoadingOnRefresh keep old articles visible?

level: middleimportance: should knowfreq 44%

basics

~20 s

After invalidate or refresh, the AsyncValue stays AsyncData holding the old articles with isLoading and isRefreshing true, and when() shows data because skipLoadingOnRefresh defaults to true. A watched dependency change instead yields AsyncLoading with the old value (isReloading).

open as a page

In Riverpod 3, how does AsyncValue.requireValue let a provider combine two async providers without await, and when is it dangerous inside a widget?

level: seniorimportance: should knowfreq 28%

basics

~20 s

requireValue returns the data if any exists, rethrows the error wrapped in ProviderException, or throws AsyncValueIsLoadingException while loading. Inside a FutureProvider or AsyncNotifier that loading exception is silenced, so ref.watch(a).requireValue combines providers synchronously; in a widget it crashes the build.

open as a page

In Riverpod 3, what are the experimental mutations, and how would you show a spinner on a news article's bookmark button while the save runs?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Mutations are an experimental Riverpod 3 API for tracking a side effect's progress outside provider state. A Mutation keyed per article is watched for MutationIdle/Pending/Success/Error and started with run(ref, (tsx) async ...), so the button shows a spinner while pending.

open as a page