riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

AsyncValue.error apparently lost after AsyncValue.guard + state=

Open motucraft opened this issue 1 year ago • 4 comments

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?

motucraft avatar Apr 02 '23 22:04 motucraft

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.

kmcgill88 avatar May 08 '23 21:05 kmcgill88

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. 🤷🏻‍♂️

kmcgill88 avatar May 22 '23 19:05 kmcgill88

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

rrousselGit avatar Nov 13 '23 12:11 rrousselGit

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_

Patrick386 avatar Dec 04 '23 13:12 Patrick386

In 3.0 calling state= or using ref after the notifier was disposed results in an exception.

I'll therefore close this

rrousselGit avatar Mar 01 '24 12:03 rrousselGit