Exception thrown by async function is intercepted by crashlytics - flutter

I have the following function:
class CannotOpenMapException implements Exception {}
void launchMap(String address) async {
…
throw CannotOpenMapException();
}
And then in an onTap handler:
onTap: () {
try {
launchMap(my_address);
} on CannotOpenMapException {
print('caught exception!');
}
}
Thing is, the exception is not caught, the print statement is never executed.
I think the problem is with the way I start Crashlytics (though this is how the official docs recommend it):
void main() async {
runZonedGuarded<Future<void>>(() async {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
…
runApp(const MyApp());
},
(error, stack) =>
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}
Is there a way I can make Crashlytics only catch the exception I don't catch?

You should use the second argument "onError" of runZoneGuarder, this way only the exceptions that's you don't catch are intercepted.
runZonedGuarded<Future<void>>(() async {
WidgetsFlutterBinding.ensureInitialized();
... and stuff
runApp(const MyApp());
}, (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack);
});
The doc:
https://api.flutter.dev/flutter/dart-async/runZonedGuarded.html

I found the problem: The onTap handler was not async, so it just started launchMap and then exited. Now the launchMap execution was running "detached" (not sure that is the correct word for it). In order to catch it, onTap needs to be async and use the await keyword:
onTap: () async {
try {
await launchMap(widget.listing.address());
} on CannotOpenMapException {
…
}
})

onTap: () {
try {
await launchMap(my_address);
} on CannotOpenMapException {
print('caught exception!');
}
}
if you want to use async then use await.

Related

Why exceptions thrown by an awaited async function are not caught by the try catch block?

If you run this in DartPad you'll never catch the exceptions, they are reported as unhandled in the console. Why?
import 'dart:html';
void main() async {
try {
final socket = await wsConnect('wss://127.0.0.1:7654');
print('${socket.readyState}');
} catch (e) {print(e);}
}
Future<WebSocket> wsConnect(String url) async {
final socket = WebSocket(url);
socket.onError.listen((_) => throw Exception('connection error'));
socket.onClose.listen((_) => throw Exception('connection close'));
return socket;
}
This doesn't work either:
import 'dart:html';
void main() async {
try {
final socket = await wsConnect('wss://127.0.0.1:7654', () => throw Exception('some problem'));
print('${socket.readyState}');
} catch (e) {print(e);}
}
Future<WebSocket> wsConnect(String url, void Function() onProblem) async {
final socket = WebSocket(url);
socket.onError.listen((_) => onProblem());
socket.onClose.listen((_) => onProblem());
return socket;
}
Are the stream callbacks being executed on some separate zone, isolate or VM or whatnot and where can I read about it?

Dart: Why is an async error not caught when it is thrown in the constructor body?

main() async {
try {
final t = Test();
await Future.delayed(Duration(seconds: 1));
} catch (e) {
// Never printed
print("caught");
}
}
void willThrow() async {
throw "error";
}
class Test {
Test() {
willThrow();
}
}
If the "async" keyword is removed from willThrow everything works as expected.
Is it because you can't await a constructor? If so is there anyway to catch async errors in a constructor body?
Have this a go:
void main() async {
try {
final t = Test();
await Future.delayed(Duration(seconds: 1));
} catch (e) {
// Never printed
print("caught");
}
}
Future<void> willThrow() async {
throw "error";
}
class Test {
Test() {
willThrow().catchError((e){print('Error is caught here with msg: $e');});
}
}
As to the 'why':
You use a normal try/catch to catch the failures of awaited asynchronous computations. But since you cannot await the constructor, you have to register the callback that handles the exception in another way. I think :)
Since you never awaited the Future that was returned from willThrow(), and you never used the result of the Future, any exception thrown by the function is discarded.
There is no way to write an asynchronous constructor. So you are stuck with using old-school callbacks to handle errors, or simulate an async constructor with a static method:
void main() async {
try {
final t = await Test.create();
await Future.delayed(Duration(seconds: 1));
} catch (e) {
// Never printed
print("caught");
}
}
Future<void> willThrow() async {
throw "error";
}
class Test {
Test._syncCreate() {}
Future<void> _init() async {
await willThrow();
}
static Test create() async {
Test result = Test._syncCreate();
await result._init();
return result;
}
}

How to resolve emit was called after an event handler completed normally bloc error?

I am using flutter bloc to make download progress percentage displayed but I keep getting this problem. I figured the problem arises in the onDone method but I couldn't figure out how to fix it.
ERROR :
Exception has occurred.
_AssertionError ('package:bloc/src/bloc.dart': Failed assertion: line 137 pos 7: '!_isCompleted':
emit was called after an event handler completed normally.
This is usually due to an unawaited future in an event handler.
Please make sure to await all asynchronous operations with event handlers
and use emit.isDone after asynchronous operations before calling emit() to
ensure the event handler has not completed.
BAD
on<Event>((event, emit) {
future.whenComplete(() => emit(...));
});
GOOD
on<Event>((event, emit) async {
await future.whenComplete(() => emit(...));
});
)
CODE :
import 'package:bloc/bloc.dart';
import 'package:download_progress_with_bloc/downlaod_file.dart';
import 'package:download_progress_with_bloc/download_event.dart';
import 'package:download_progress_with_bloc/download_state.dart';
import 'package:download_progress_with_bloc/permission_handler.dart';
import 'package:download_progress_with_bloc/store_book_repo.dart';
import 'package:http/http.dart' as http;
class DownloadBloc extends Bloc<DownloadEvent, DownloadState> {
DownloadBloc({required this.storeBookRepo}) : super(DownloadInitial()) {
on<DownloadStarted>(onStarted);
on<DownloadProgressed>(onProgressed);
}
final StoreBookRepo storeBookRepo;
http.StreamedResponse? response;
// StreamController? _controller;
int received = 0;
List<int> bytes = [];
int totalSize = 0;
#override
Future<void> close() {
return super.close();
}
Future<void> onStarted(
DownloadStarted event, Emitter<DownloadState> emit) async {
try {
await PermissionHandler.requestStoragePermission();
response = await downloadFile();
totalSize = response!.contentLength ?? 0;
emit(DownloadInProgress(progress: received, totalSize: totalSize));
response?.stream.asBroadcastStream().listen((value) async {
received += value.length;
bytes.addAll(value);
add(DownloadProgressed(progress: received));
print('received value is $received');
}).onDone(
() async {
await storeBookRepo
.storePdf(
bytes.toString(),
bookTitle: 'bookTitle',
)
.then((value) => emit(DownloadCompleted()));
// emit(DownloadCompleted());
},
);
} catch (e) {
emit(DownlaodFailed(errorMessage: '$e'));
}
}
void onProgressed(DownloadProgressed event, Emitter<DownloadState> emit) {
emit(DownloadInProgress(progress: event.progress, totalSize: totalSize));
}
}
What if rewrite listen to await for like this?
Future<void> onStarted(
DownloadStarted event,
Emitter<DownloadState> emit,
) async {
try {
await PermissionHandler.requestStoragePermission();
response = await downloadFile();
totalSize = response!.contentLength ?? 0;
emit(DownloadInProgress(
progress: received,
totalSize: totalSize,
));
await for (final value in response?.stream) {
received += value.length;
bytes.addAll(value);
add(DownloadProgressed(progress: received));
print('received value is $received');
}
await storeBookRepo.storePdf(
bytes.toString(),
bookTitle: 'bookTitle',
);
emit(DownloadCompleted());
} catch (e) {
emit(DownlaodFailed(errorMessage: '$e'));
}
}
use async before on bloc and await for Future operations
on<NumberTriviaEvent>((event, emit) async {
await Future func(){//some time consuming operation like fetch data}
emit(YourState())
}
you should use async and await , its clear.
but why?
before complete run of each on() method, you can use emiteer. after that bloc will dispose or cancel that emitter.

How to get the value from Future.error inside catchError?

void main() {
foo().catchError((error) {
if (error is Future) {
error.then((value) => print('value = $value'));
}
});
}
Future<void> foo() async {
throw Future.error('FooError');
}
The error is caught inside catchError but I am not able to retrieve the value of Future.error which is FooError in this example.
Well that's Future.error so you'll have to again use catchError on it.
void main() {
foo().catchError((error) {
if (error is Future) {
error.catchError((error) => print(error)); // prints 'FooError'
}
});
}
Future<void> foo() async {
throw Future.error('FooError');
}
You just don't, there is no point in doing so.
You can either throw the error:
void main() {
foo().catchError((error) {
print('error = $error');
});
}
Future<void> foo() async {
throw 'FooError';
}
Or if that is for whatever reason not convinient, you can use Future.error to create a Future that will have an error already:
void main() {
foo().catchError((error) {
print('error = $error');
});
}
Future<void> foo() {
return Future.error('FooError');
}
But actually throwing a Future.error is redundant and not useful.

Sentry is NOT reporting error inside Flutter

I have my Sentry setup like this:
void main() => runZonedGuarded(() {
runApp(MyApp());
}, (Object error, StackTrace stackTrace) {
reportError(error, stackTrace);
});
and related functions
final SentryClient sentry = new SentryClient(dsn: '<my-dsn>');
Future<void> reportError(dynamic error, dynamic stackTrace) async {
sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
}
I added throw Exception("my-error") inside a widget's build method, I can't see the error is showing on the Sentry web console.
I create a single file to throw exception and sentry capture, and I do see sentry is reporting the error.
Something must wrong with runZonedGuarded.
Check in your sentry dashboard if you are using the free version and if the monthly quota hasnt been surpassed. If that is the case you will not receive any events.
After a number of Sentry setups that didn't seem to work right, I arrived at this one that works:
Future<void> main() async {
final sentry = Sentry.SentryClient(Sentry.SentryOptions(dsn: '[Add dsn URI here]'));
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (FlutterErrorDetails errorDetails) {
sentry.captureException(
errorDetails.exception,
stackTrace: errorDetails.stack,
);
};
runApp(MyApp());
}, (Object error, StackTrace stackTrace) {
sentry.captureException(
error,
stackTrace: stackTrace,
);
});
}
you must make the func async to send error to sentry console
be sure import this file for mobile app:
import 'package:sentry/io_client.dart';
E.g:
main.dart
import 'package:sentry/io_client.dart';
final SentryClient sentry = new SentryClient(dsn: YOUR_DSN);
main() async {
try {
throw new StateError('This is a Dart exception.');
} catch(error, stackTrace) {
await sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
}
}
HomeScreen
floatingActionButton: FloatingActionButton(
onPressed: () async{
throw new StateError('This is a Dart exception.');
},
By the way in release version it will send every exception, because in debug flutter doesn't catch every error that displays in the console, and to simplify that for you can use one of these packages :
https://pub.dev/packages/catcher
https://pub.dev/packages/flutter_sentry