skip to content

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%

answer

  1. try/catch in one line
  2. returns a Future of AsyncValue
  3. optional test predicate
  4. state = merges previous data
  5. not needed inside build

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.

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

for a junior

Know that AsyncValue.guard turns a success into AsyncData and a thrown error into AsyncError, so notifier methods need no try/catch.

for a middle

Explain the test predicate, why build needs no guard, and how assigning state merges previous data into loading and error states.

for a senior

Separate expected failures from bugs with the predicate, and design error screens that use the carried-over value instead of blanking content.

for a principal

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