| name | riverpod-ref-in-provider-lifecycle |
| description | Fix Riverpod "Cannot use Ref or modify other providers inside life-cycles/selectors" crash
in @riverpod provider bodies. Use when: (1) Crash during provider disposal with this exact
error message, (2) Using keepAlive() with timer-based disposal, (3) Async callbacks (.then,
.catchError) try to use ref.read() or ref.invalidateSelf(), (4) Error occurs after
ref.onCancel/onResume/onDispose callbacks fire. Solution: wrap ref operations in try-catch
or avoid using ref in async callbacks entirely.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-01T00:00:00.000Z" |
Riverpod ref Usage in Provider Lifecycle Callbacks
Problem
When using ref.keepAlive() with timer-based auto-disposal in @riverpod providers,
async callbacks (.then(), .catchError()) that execute during or after disposal
will crash when they try to use ref.read(), ref.invalidateSelf(), or any other
ref operation.
Context / Trigger Conditions
Error message:
'package:riverpod/src/core/ref.dart': Failed assertion: line 216 pos 7:
'_debugCallbackStack == 0': Cannot use Ref or modify other providers inside life-cycles/selectors.
Typical scenario:
- Provider uses
ref.keepAlive() with a timer in ref.onCancel():
final link = ref.keepAlive();
ref.onCancel(() {
Timer(Duration(seconds: 15), () {
link.close(); // Triggers disposal
});
});
- Provider has async callbacks that use
ref:
someAsyncOperation().then((_) {
if (ref.mounted) { // THIS CHECK IS NOT ENOUGH!
ref.invalidateSelf();
}
});
- Timer fires while async callback is pending
- CRASH -
ref.mounted returns true but using ref is still forbidden
Key insight: ref.mounted returns true during lifecycle callbacks, but
using ref operations is still forbidden. This is counter-intuitive but by design.
Solution
Option 1: Wrap ref operations in try-catch (Recommended)
someAsyncOperation().then((_) {
try {
ref.read(someProvider.notifier).update(/*...*/);
} catch (e) {
Log.debug('Provider likely disposed: $e');
}
});
Option 2: Avoid ref operations in async callbacks entirely
Instead of:
// BAD - uses ref in async callback
openVineVideoCache.removeCorruptedVideo(videoId).then((_) {
if (ref.mounted) {
ref.invalidateSelf(); // CRASH!
}
});
Do:
// GOOD - fire-and-forget without ref
unawaited(
openVineVideoCache.removeCorruptedVideo(videoId).then((_) {
Log.info('Cache removed'); // No ref usage
}),
);
// Let user retry manually or provider recreates on next access
Option 3: Capture provider state synchronously first
// Read state BEFORE async operation
final notifier = ref.read(someProvider.notifier);
// Use captured reference in callback (not ref)
someAsyncOperation().then((_) {
notifier.doSomething(); // Uses captured reference, not ref
});
Anti-Pattern: ref.mounted Check
// THIS DOES NOT WORK!
someAsyncOperation().then((_) {
if (ref.mounted) { // Returns true during lifecycle!
ref.invalidateSelf(); // Still crashes!
}
});
The ref.mounted check is insufficient because:
- Timer fires,
link.close() called
- Riverpod enters disposal lifecycle (callback stack > 0)
ref.mounted check passes (still returns true!)
ref.invalidateSelf() called
- CRASH - ref operations forbidden during lifecycle callbacks
Verification
- Rapidly scroll through content that uses the provider
- Navigate away and back while providers are active
- Let the keepAlive timers fire naturally (wait 15+ seconds after scrolling)
- Check Crashlytics/console for the lifecycle assertion error
- Error should no longer occur
Example
Before (causes crash):
@riverpod
VideoPlayerController videoController(Ref ref, String videoId) {
final link = ref.keepAlive();
ref.onCancel(() {
Timer(Duration(seconds: 15), () => link.close());
});
final controller = VideoPlayerController.networkUrl(url);
controller.initialize().catchError((error) {
// CRASH! This callback may run during disposal
if (ref.mounted) {
ref.invalidateSelf(); // Assertion failure!
}
});
return controller;
}
After (safe):
@riverpod
VideoPlayerController videoController(Ref ref, String videoId) {
final link = ref.keepAlive();
ref.onCancel(() {
Timer(Duration(seconds: 15), () => link.close());
});
final controller = VideoPlayerController.networkUrl(url);
controller.initialize().catchError((error) {
// Safe - wrapped in try-catch
try {
ref.read(fallbackProvider.notifier).state = newValue;
} catch (e) {
Log.debug('Provider disposed during error handling: $e');
}
// Don't invalidateSelf - let provider recreate on next access
});
return controller;
}
Notes
- This issue is specific to
@riverpod provider bodies, not widget dispose()
- The related skill
riverpod-ref-read-in-dispose covers widget lifecycle issues
- Consider whether
ref.invalidateSelf() is even necessary - often the provider
will be recreated naturally on next access
- For truly critical cleanup, use
ref.onDispose() which runs synchronously
before the lifecycle callback stack check
- This bug is particularly common with video players, image loaders, and other
resources that use keepAlive() with timer-based disposal
Related Skills
riverpod-ref-read-in-dispose: For widget dispose() ref.read() issues
flutter-dispose-timer-test-failure: For timer-related test failures
References