When an autoDispose async provider is disposed before it emits its first
value, Riverpod reports:
StateError: The provider ... was disposed during loading state, yet no value could be emitted.
I understand that Riverpod may want to complete pending awaiters with an error
instead of leaving them unresolved forever.
What I do not understand is why this is modeled as a StateError instead of a
dedicated catchable cancellation/disposal exception.
This is difficult to reason about, especially for Riverpod newcomers, because:
StateError is an Error, not an Exception
- in Dart,
Errors usually represent programmer mistakes or broken invariants
- this specific case can happen during normal navigation/lifecycle teardown
- it makes it unclear whether the app has a real bug or whether this is
expected
- it makes benign disposal harder to handle cleanly in error reporting
Minimal repro
main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
ProviderScope(
observers: [DisposeObserver()],
child: const App(),
),
);
}
final delayedStreamProvider = StreamProvider.autoDispose<int>((ref) async* {
debugPrint('delayedStreamProvider: started');
ref.onDispose(() {
debugPrint('delayedStreamProvider: disposed');
});
await Future<void>.delayed(const Duration(seconds: 3));
debugPrint('delayedStreamProvider: emitting first value');
yield 1;
}, name: 'delayedStreamProvider');
final dependentProvider = FutureProvider.autoDispose<String>((ref) async {
debugPrint('dependentProvider: awaiting delayedStreamProvider.future');
final value = await ref.watch(delayedStreamProvider.future);
return 'Resolved with $value';
}, name: 'dependentProvider');
final class DisposeObserver extends ProviderObserver {
@override
void providerDidFail(
ProviderObserverContext context,
Object error,
StackTrace stackTrace,
) {
debugPrint(
'providerDidFail: ${context.provider.name} ${context.provider.argument}',
);
debugPrint('error: $error');
debugPrint('stackTrace: $stackTrace');
}
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const ReproScreen(),
);
}
}
class ReproScreen extends ConsumerWidget {
const ReproScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(dependentProvider);
return Scaffold(
appBar: AppBar(title: const Text('Riverpod Dispose Repro')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
Text('Current state: $state'),
FilledButton(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => const FinishedScreen(),
),
);
},
child: const Text('Replace screen before first value'),
),
],
),
),
);
}
}
class FinishedScreen extends StatelessWidget {
const FinishedScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('After dispose')),
body: const Center(
child: Text('Check the console output from ProviderObserver.'),
),
);
}
}
Repro steps
- Run the app.
- Tap
Replace screen before first value within 3 seconds.
- Observe the provider failure in the console.
Questions
- Is this behavior intentional?
- What is the recommended official way to handle this case?
- Why is this modeled as
StateError instead of a dedicated
cancellation/disposal exception?
- Is there an official pattern for treating this as benign lifecycle teardown
without brittle message matching?
Possible improvement
If this behavior is intentional, would Riverpod consider using a dedicated
catchable exception type for this case instead of StateError?
That would make it easier to distinguish:
- real application bugs / broken invariants
- expected disposal/cancellation caused by normal navigation or lifecycle
changes
and would likely make this behavior much easier to understand for newcomers.
When an
autoDisposeasync provider is disposed before it emits its firstvalue, Riverpod reports:
I understand that Riverpod may want to complete pending awaiters with an error
instead of leaving them unresolved forever.
What I do not understand is why this is modeled as a
StateErrorinstead of adedicated catchable cancellation/disposal exception.
This is difficult to reason about, especially for Riverpod newcomers, because:
StateErroris anError, not anExceptionErrors usually represent programmer mistakes or broken invariantsexpected
Minimal repro
main.dart
Repro steps
Replace screen before first valuewithin 3 seconds.Questions
StateErrorinstead of a dedicatedcancellation/disposal exception?
without brittle message matching?
Possible improvement
If this behavior is intentional, would Riverpod consider using a dedicated
catchable exception type for this case instead of
StateError?That would make it easier to distinguish:
changes
and would likely make this behavior much easier to understand for newcomers.