riverpod
riverpod copied to clipboard
AsyncValue.error apparently lost after AsyncValue.guard + state=
Discussed in https://github.com/rrousselGit/riverpod/discussions/2418
Originally posted by motucraft April 1, 2023 Hi there.
I have a question about the behavior when an error occurs in AsyncNotifier.
Here is a simple sample.
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Sample(),
);
}
}
class Sample extends ConsumerWidget {
const Sample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: SafeArea(
child: Center(
child: ElevatedButton(
onPressed: () => ref.read(sampleNotifierProvider.notifier).sample(),
child: const Text('push me'),
),
),
),
);
}
}
@riverpod
class SampleNotifier extends _$SampleNotifier {
@override
FutureOr<bool?> build() async => null;
Future<void> sample() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// error as a sample
int.parse('str');
// await throwException();
return true;
});
print('hasError=${state.hasError}');
print('state=$state');
}
}
Future<void> throwException() async {
try {
// error as a sample
await http.Client().get(Uri.parse('https://error'));
} catch (error, _) {
print(error.runtimeType);
print('error occurred. $error');
rethrow;
}
}
Tapping "push me" will result in an error and output as follows. This is to be expected.
flutter: hasError=true
flutter: state=AsyncError<bool?>(value: null, error: FormatException: Invalid radix-10 number (at character 1)
str
^
, stackTrace: #0 int._handleFormatError (dart:core-patch/integers_patch.dart:126:5)
#1 int._parseRadix (dart:core-patch/integers_patch.dart:137:16)
#2 int._parse (dart:core-patch/integers_patch.dart:98:12)
#3 int.parse (dart:core-patch/integers_patch.dart:60:12)
#4 SampleNotifier.sample.<anonymous closure> (package:riverpod_ref_read/main.dart:54:11)
#5 AsyncValue.guard (package:riverpod/src/common.dart:166:42)
#6 SampleNotifier.sample (package:riverpod_ref_read/main.dart:52:30)
#7 Sample.build.<anonymous closure> (package:riverpod_ref_read/main.dart:36:72)
#8 _InkResponseState.handleTap (package:flutter/src/material/ink_well.dart:1096:21)
#9 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:253:24)
#10 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:627:11)
#11 BaseTapGe<…>
Now, what happens if I comment out int.parse('str');
and execute await throwException();
?
state = await AsyncValue.guard(() async {
// error as a sample
// int.parse('str');
await throwException();
return true;
});
The output is as follows.
flutter: _ClientSocketException
flutter: error occurred. Failed host lookup: 'error'
flutter: hasError=false
flutter: state=AsyncData<bool?>(value: null)
Errors are indeed occurring. And yet, "hasError=false". state is also an AsyncData. I do not understand why this is happening.
Changing the code as follows will result in the output below.
Future<void> sample() async {
state = const AsyncValue.loading();
final newState = await AsyncValue.guard<bool>(() async {
// error as a sample
await throwException();
return true;
});
state = newState;
print('newState.hasError=${newState.hasError}');
print('state.hasError=${state.hasError}');
}
}
flutter: _ClientSocketException
flutter: error occurred. Failed host lookup: 'error'
flutter: newState.hasError=true
flutter: state.hasError=false
The value of "hasError" is different between state and newState.
Why is this the result? Am I overlooking something simple?
Unfortunately, I've run into this exact issue on hooks_riverpod: ^2.2.0
. I saw 2.3.6
was available so I bumped it up, still happening.
I was able to work around this by adding a ref.watch(sampleNotifierProvider)
in the widgets build
function. By adding the ref.watch
in the build, the provider can be instantiated prior to invoking ref.read(sampleNotifierProvider.notifier).sample()
. Without doing this the provider's build
wasn't guaranteed to be completed before sample()
completes. My best guess is a race condition. 🤷🏻♂️
This appears to be a side-effect of autoDispose.
Your notifier was not listened, so during the await of the network request, your notifier got destroyed. And from there, any usage of state=
is ignored.
This behavior is funky. We should likely throw instead of having the setter not do anything. But this is a breaking change. So flagging that for v3
This issue occurs similarly in AsyncNotifier. It doesn't handle unexpected errors when fetching data from the server.
ℹ️ INFO: Fetch error:Unexpected null value.
ℹ️ INFO: items:AsyncError<List<TestModel>>(error: UnimplementedError, stackTrace: dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 294:3 throw_
In 3.0 calling state=
or using ref
after the notifier was disposed results in an exception.
I'll therefore close this