In a Riverpod 3 AsyncNotifier, what does AsyncValue.guard do, and what does state hold if a guarded news-feed refresh fails?
answer
- try/catch in one line
- returns a Future of AsyncValue
- optional test predicate
- state = merges previous data
- not needed inside build
basics
~20 sAsyncValue.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.
solid answer
~40 s`AsyncValue.guard(() async => ...)` awaits the callback and returns a `Future<AsyncValue<T>>`: `AsyncData` with the result, or `AsyncError` with the error and stack trace. An optional second argument, `bool Function(Object)`, limits which errors are captured; others are rethrown. The usual notifier method is `state = const AsyncLoading(); state = await AsyncValue.guard(() => repo.fetchHeadlines());`. Assigning `state` on an async notifier merges with the previous state, so the loading state still carries the old articles (`isReloading` is true), and if the fetch throws, the `AsyncError` keeps them too - `hasValue` and `hasError` are both true and `value` is the old list. That lets the screen keep content and show the error. `build` itself needs no `guard`: a throwing `build` already becomes `AsyncError`. For synchronous code, `AsyncResult.guard` is the non-`Future` variant.
code
dart · 21 linesimport 'package:flutter_riverpod/flutter_riverpod.dart';
class NewsFeed extends AsyncNotifier<List<Article>> {
@override
Future<List<Article>> build() {
return ref.watch(newsApiProvider).fetchHeadlines();
}
Future<void> refreshHeadlines() async {
state = const AsyncLoading();
final next = await AsyncValue.guard(
() => ref.read(newsApiProvider).fetchHeadlines(),
(error) => error is! FormatException,
);
if (!ref.mounted) return;
state = next;
}
}
final newsFeedProvider =
AsyncNotifierProvider<NewsFeed, List<Article>>(NewsFeed.new);go deeper
Know that AsyncValue.guard turns a success into AsyncData and a thrown error into AsyncError, so notifier methods need no try/catch.
Explain the test predicate, why build needs no guard, and how assigning state merges previous data into loading and error states.
Separate expected failures from bugs with the predicate, and design error screens that use the carried-over value instead of blanking content.
Agree on an error taxonomy across repositories so guards capture the same classes of failure everywhere and crash reporting sees real bugs.
## The problem guard removes An `AsyncNotifier` method that performs work must turn success into `AsyncData` and failure into `AsyncError`. Written by hand: ```dart Future<void> refreshHeadlines() async { state = const AsyncLoading(); try { final articles = await ref.read(newsApiProvider).fetchHeadlines(); state = AsyncData(articles); } catch (error, stackTrace) { state = AsyncError(error, stackTrace); } } ``` **`AsyncValue.guard`** folds the `try`/`catch` into one expression: ```dart Future<void> refreshHeadlines() async { state = const AsyncLoading(); state = await AsyncValue.guard( () => ref.read(newsApiProvider).fetchHeadlines(), ); } ``` ## Exact behaviour - Signature: `static Future<AsyncValue<T>> guard<T>(Future<T> Function() future, [bool Function(Object)? test])`. - On success: `AsyncData(result)`. - On a thrown error with no `test`: `AsyncError(error, stackTrace)`. - With a `test`: errors for which `test` returns true become `AsyncError`; the rest are **rethrown** with their original stack trace. Example: `(err) => err is! FormatException` lets a parsing bug crash loudly while network errors become state. - `AsyncResult.guard(() => ...)` is a synchronous sibling returning `AsyncResult` (`AsyncData` or `AsyncError`). ## What state holds at each step Assigning `state` on an async notifier does not simply replace it. Riverpod merges the new state with the previous one, so data survives transitions: | Moment | Runtime type | `value` | Flags | |---|---|---|---| | Before the call | `AsyncData` | old articles | - | | After `state = const AsyncLoading()` | `AsyncLoading` | old articles | `isLoading`, `isReloading` | | Guard succeeded | `AsyncData` | new articles | - | | Guard failed | `AsyncError` | old articles | `hasError`, `hasValue` | Consequences for the news screen: 1. A widget using `when` with defaults shows the spinner during the manual loading state, because it counts as a reload. 2. After a failure, a type-pattern switch hits the `AsyncError` branch, yet `feed.value` still has the old list - so the error branch can render the list with a banner. 3. If you want the failure to wipe the list, call `unwrapPrevious()` when rendering. ## Where not to use guard - **In `build`**: a `Future` returned from `build` that throws already becomes `AsyncError`. The migration guide from `StateNotifier` says to remove `guard` from `build`. - **Around code whose errors must reach the caller**: `guard` swallows them into state. A method that must tell its caller about failure should rethrow or return a result. ## Related habits - Check that the notifier is still mounted after long awaits before writing `state` - that is a `Ref` concern. - Keep `guard` callbacks small; one per logical operation makes errors attributable. ## guard versus AsyncNotifier.update `guard` is for operations that should put the provider into a loading state and then either data or error. Some updates should not: an optimistic 'mark as read' should keep showing the feed and only report failures elsewhere. For those, an `AsyncNotifier` offers other tools, and the choice between them belongs with notifier design; the point for `AsyncValue` is that `guard` always produces a full data-or-error transition. ## Checklist for a guarded method 1. Decide whether the UI should show loading during the operation; if not, skip `state = const AsyncLoading()`. 2. Pass a `test` predicate so programming errors are not disguised as network failures. 3. Check that the notifier is still mounted after the await before assigning `state`. 4. Decide how the screen renders an error that still carries old data - banner over the list, or a full error view via `unwrapPrevious()`. 5. Keep the guarded callback to one operation so a failure points at one cause.
- Why pass a test predicate to AsyncValue.guard?To keep programming errors out of UI state. Network and server failures are expected and belong in `AsyncError`; a `FormatException` from a changed JSON shape or a `TypeError` is a bug that should surface in crash reporting. With `(err) => err is! FormatException`, guard captures the former and rethrows the latter with its original stack trace.
- Does state = const AsyncLoading() in an AsyncNotifier erase the previously loaded articles?No. Assigning an async notifier's state merges the new value with the previous one, so the resulting `AsyncLoading` still has the old list as `value` and reports `isReloading`. Widgets using default `when` show the loading branch, but the data is available if you choose to render it.
saying these in an interview costs you the question
- AsyncValue.guard catches the error and returns null
- guard should wrap the body of every AsyncNotifier build
- A failed guard leaves state with no access to the old data
- guard's test argument is a timeout in milliseconds
- state = const AsyncLoading() always wipes the previous value