skip to content

A Riverpod 3 FutureProvider that watches the currency setting sometimes throws UnmountedRefException after an await when the user switches currency mid-request; what is happening and how do you fix it?

level: seniorimportance: should knowfreq 34%

answer

  1. each rebuild creates a new Ref
  2. the old Ref stops being usable
  3. onDispose runs before every rebuild
  4. check ref.mounted after await
  5. cancel work in onDispose

basics

~20 s

Switching currency rebuilds the provider with a new Ref, so the closure still awaiting from the previous build holds a disposed Ref; using it throws UnmountedRefException. Cancel the work in ref.onDispose, or check ref.mounted after each await.

solid answer

~40 s

A provider that `ref.watch`es `currencyProvider` is rebuilt when the currency changes: Riverpod runs its `onDispose` callbacks and calls the body again with a **new** `Ref`. The previous invocation is still suspended at its `await`, holding the old `Ref`, whose `mounted` is now false. Since 3.0, any use of a disposed `Ref` - `watch`, `read`, `listen` - throws `UnmountedRefException` instead of silently corrupting state. Two fixes, often combined: register cancellation with `ref.onDispose` right after starting the work, so the request stops as soon as the provider rebuilds, and check `if (!ref.mounted)` after each `await` before touching `ref` again, then return or throw. Also do `ref.watch` calls before the first `await`. The same applies to a Notifier method whose autoDispose provider was disposed while it awaited: setting `state` then throws.

code

dart · 16 lines
dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

class PortfolioSync extends Notifier<DateTime?> {
  @override
  DateTime? build() => null;

  Future<void> syncNow() async {
    final currency = ref.read(currencyProvider);
    await portfolioApi.upload(currency);
    if (!ref.mounted) return;
    state = DateTime.now();
  }
}

final portfolioSyncProvider =
    NotifierProvider.autoDispose<PortfolioSync, DateTime?>(PortfolioSync.new);

go deeper

for a junior

Remember that a provider may be rebuilt or disposed while it awaits, and that ref.mounted tells you whether it is still safe to use ref.

for a middle

Explain that onDispose runs before every rebuild and on disposal, and that each rebuild of a function provider gets a new Ref.

for a senior

Diagnose the race from the stack trace, fix it with onDispose cancellation plus a mounted guard, and explain why Notifier methods differ for rebuilds versus autoDispose.

for a principal

Make cancellation a convention for every provider that starts I/O, so dependency-driven rebuilds never pay for abandoned requests or leak errors into crash reports.

## The scenario A stock-portfolio app converts holdings into the user's chosen currency: ```dart final portfolioValueProvider = FutureProvider<double>((ref) async { final currency = ref.watch(currencyProvider); final rates = await ratesApi.fetch(currency); final holdings = ref.read(holdingsProvider); return holdings.valueIn(rates); }); ``` The user switches from USD to EUR while the USD request is in flight. Seconds later the app logs an `UnmountedRefException` whose message begins 'Cannot use the Ref of ... after it has been disposed'. ## What actually happens 1. `currencyProvider` notifies. Because `portfolioValueProvider` watches it, Riverpod rebuilds the provider. 2. Before rebuilding, it runs the provider's **`onDispose`** callbacks. `onDispose` fires not only on final disposal but also right before every rebuild, when an autoDispose provider loses its last listener, and when the `ProviderContainer` or `ProviderScope` is disposed. 3. The body is called again with a **new `Ref`**. The source defines `mounted` as the element not being disposed **and** the element's current ref being this very object, so the previous `Ref` now reports `mounted == false`. 4. The first invocation resumes after `await ratesApi.fetch('USD')` and calls `ref.read(holdingsProvider)` on the old `Ref`. Riverpod 3 checks every `Ref` call and throws `UnmountedRefException`. In Riverpod 2 this could slip through and cause subtle bugs; 3.0 made it an error on purpose, to surface the race. ## Fix 1: cancel in onDispose Register cleanup as soon as the work starts, so it stops when the provider rebuilds or is disposed: ```dart final portfolioValueProvider = FutureProvider<double>((ref) async { final currency = ref.watch(currencyProvider); final holdings = ref.watch(holdingsProvider); final request = ratesApi.start(currency); ref.onDispose(request.cancel); final rates = await request.result; return holdings.valueIn(rates); }); ``` The docs prefer this where the API supports cancellation: it aborts pending work earlier, while `mounted` lets the request finish and only stops the logic afterwards. Guidelines for `onDispose`: - One `onDispose` per disposable resource, next to its creation, so a missing one is visible and an exception in one does not skip the others. - Do not use `ref` inside an `onDispose` callback; the provider is already being torn down, and Riverpod asserts against using `Ref` inside life-cycles. ## Fix 2: check ref.mounted after await `Ref.mounted`, new in 3.0, mirrors `BuildContext.mounted`: ```dart final rates = await ratesApi.fetch(currency); if (!ref.mounted) throw const CancelledException(); ``` The docs show both returning early and throwing a custom exception you then ignore in error reporting. Either way nothing touches the stale `Ref`. ## Fix 3: watch before the first await Moving every `ref.watch` and `ref.read` above the first `await` shrinks the window in which a stale `Ref` can be used. It does not remove the need for fix 1 or 2 when work after the `await` still needs `ref`. ## Notifiers are slightly different | Situation | What happens | |---|---| | Function provider rebuilds while awaiting | the captured `ref` is a previous one: `mounted` false, calls throw | | `Notifier` method awaits, provider rebuilds meanwhile | `ref` getter returns the current `Ref`, so `mounted` stays true | | `Notifier` method awaits, autoDispose provider disposed meanwhile | `mounted` false; setting `state` throws | So inside a `Notifier` or `AsyncNotifier` method, `if (!ref.mounted) return;` after an `await` guards against the screen that owned an autoDispose provider being closed mid-request. ## Diagnosing it in production - The exception's stack points at the line after an `await` in a provider body or notifier method. - Reproduce by triggering the dependency change, or leaving the screen, while the request is slow. - Add the `onDispose` cancellation and the `mounted` guard, then sweep other providers with the same shape.

  • When exactly do Ref.onDispose callbacks run in Riverpod 3?
    Right before the provider's state is destroyed: before every rebuild triggered by a watched dependency, `invalidate` or `refresh`; when an autoDispose provider loses its last listener; and when the owning `ProviderContainer` or `ProviderScope` is disposed. That is why they are the right place to cancel in-flight requests and close timers or stream subscriptions.
  • Why do the Riverpod docs prefer onDispose cancellation over a ref.mounted check?
    `onDispose` stops the work while it is still running, so a slow request is aborted as soon as the provider rebuilds. A `mounted` check lets the request run to completion and only skips the logic afterwards, wasting the round trip. Use `mounted` when the API cannot be cancelled, or as a second guard.

saying these in an interview costs you the question

  • onDispose only runs when the provider is removed for good
  • The same Ref object is reused across every rebuild of a function provider
  • Wrapping the await in try/catch is the proper fix for UnmountedRefException
  • It is fine to call ref.read inside an onDispose callback to save state
  • Ref.mounted is only available on WidgetRef