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?
answer
- each rebuild creates a new Ref
- the old Ref stops being usable
- onDispose runs before every rebuild
- check ref.mounted after await
- cancel work in onDispose
basics
~20 sSwitching 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 sA 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 linesimport '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
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.
Explain that onDispose runs before every rebuild and on disposal, and that each rebuild of a function provider gets a new Ref.
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.
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