- What is an exception?
- Exceptions handling in different languages
- Exception handling in Dart and Flutter
- * Synchronous code
- * Asynchronous code
- * Zones and runZonedGuarded
- * Error handling in streams
- * Unhandled exceptions
- Wrap up
Sorry for the clickbait title of the article, I’m experimenting with different styles… But I’d like to not disappoint you and will provide all those 3 things :)
What is an exception?
There are two types of exception definition:
- An exception is an inability of an operation to fulfill its contract
- An exception is an unexpected behavior
It seems that the first definition can include the second one as the unexpected behavior can be one of the reasons why the operation couldn’t fulfill its contract. The other reasons could include more or less expected problems — like connection loss, corruption of the file, and so on.
As an example let’s consider this function:
Future<Money> getRemainingAmount({Account account}) async {
final response = await backend.getAcccountInfo(account);
return response.remainingAmount;
}
This function returns the remaining money in our account. In order to do this, the function should hit our backend and request the account balance and this operation can fail because of various reasons — the network is down, the server is down, the contract between frontend and backend was changed, etc… All those reasons lead to an inability of our operation to fulfill its contract. However, all of those problems can be foreseen and we can add them to the contract. The only thing we can’t expect is unexpected behavior :). For instance, we anticipated all the problems except one: when backend in some cases returns text “negative” instead of the balance of our account — so this becomes unexpected for us (it’s a completely artificial example but just to give you an idea).
The more we know about our system the less chance some unexpected behavior occurs.
The next question is what we are going to do in all those exceptional cases. Do we want to react differently for each case or maybe we don’t care much if the server is down or returns some unexpected data? It’s the question of our system design but anyway we need a way to catch those exceptions separately or as a general exceptional situation so that we can respond accordingly.
Exceptions handling in different languages
The above considerations were taken into account differently in different programming languages. Let’s consider three approaches that Go, Rust and Dart use to handle exceptions (I'm not an expert in Go and Rust so feel free to comment if you found a problem with the code).
Dart:
try {
final remaingAmount = await getRemainingAmount(account: account);
displayAccountAmount(remaingAmount);
} on SocketException catch (_) {
displayNoConnectionError();
} catch (e, s) {
logException(exception: e, stackTrace: s);
rethrow;
}
Go:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
Rust:
use std::fs::File;
fn main() {
let f = File::open("main.jpg"); // main.jpg doesn't exist
match f {
Ok(f)=> {
println!("file found {:?}",f);
},
Err(e)=> {
println!("file not found \n{:?}",e); //handled error
}
}
println!("end of main");
}
- The most common approach which was used in C++ and is being used in Dart is to pay attention mainly to the happy path. So the code is rather easy to read and it’s not contaminated with many error branches. On the other side, it’s not that clear which block of code can cause an error and what kind of error it could be. For instance, in the above example, if you look inside the function getRemainingAmount you, most likely, will not be able to say if it would throw an exception or not.
- In Go it’s all the way around, the main focus is on error handling. So a function, besides a value, should return an error that has to be handled right after a function call. If the function can’t produce an error it should return a result only, and, it will clearly indicate that function is error-free. There is no way to silently ignore an error in a hope that upstream code will handle it — sure thing you can ignore an error but that error will not be automatically propagated to the upstream, you have to do it explicitly.
- In Rust, there is the third way of dealing with exceptions/errors. It looks similar to Go: function can return Either type with value OR error and then you can use pattern matching to understand if it’s value or error and what kind of error was returned. It’s not mandatory but there is no other way to throw an exception (except panic but it’s another story) so that you probably want to return Either type from all the functions that can cause errors. Code looks really readable and you can’t just pass exception/error upstream without handling it (only if the exception type is exactly the same as in an upstream function).
Any of those three approaches can be used in your code in Dart, you can return an object with value AND error, or result with Either value OR error, or use traditional try-catch blocks and other options Dart provides you out of the box.
Let’s explore what exceptions handling options Dart provides us with.
Exception handling in Dart and Flutter
What are the situations where we need to use exception handling?
- Synchronous code
- Asynchronous code
- Timers
- Streams
- Flutter app as a whole
Synchronous code
The main tool for handing the exceptions in Dart is try-catch-finally
construction — the same as in many other languages.
In order to catch all the exceptions in a block of code you wrap the code in try block and use multiple on-catch
instructions to handle some specific exceptions, then use catch to handle all other unexpected exceptions, and finally use finally
to run the code that should be invoked after the code in try block and in any of the catch
blocks. Code in finally
will be called even if you return from try
or catch
blocks or re-throw
the exception in catch
block.
Instruction rethrow
helps you to throw the same exception after you handle it so the upstream code will take care of it.
try {
final _ = 100 ~/ 0;
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} finally {
print('Clean-up done 1');
}
print('\n\n');
try {
throw FormatException('format is wrong');
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} on FormatException catch (e) {
print('Format exceptions $e');
} finally {
print('Clean-up done 2');
}
print('\n\n');
try {
print('Allocate something 3');
throw 'Other error occurred';
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} on FormatException catch (e) {
print('Format exceptions: $e');
} catch (e, s) {
print('Unknown exceptions: $e, with stack: \n$s');
rethrow;
} finally {
print('Clean-up something 3');
}
Output:
Cant divide to zero
Clean-up done 1
Format exceptions FormatException: format is wrong
Clean-up done 2
Allocate something 3
Unknown exceptions: Other error occurred, with stack:
#0 main (file:///Users/user/Downloads/dart_sample/bin/sync_exceptions.dart:26:5)
#1 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:299:32)
#2 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
Clean-up something 3
Unhandled exception:
Other error occurred
#0 main (file:///Users/user/Downloads/dart_sample/bin/sync_exceptions.dart:26:5)
#1 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:299:32)
#2 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
We can see that all three try-catch
blocks catch the exceptions and in the third block, the exception was re-thrown
after clean-up was done.
This is pretty much it for the handling exceptions in synchronous blocks of code. You can check for more details here — https://dart.dev/guides/language/language-tour#catch
Asynchronous code
Let’s consider this example:
try {
Future.delayed(Duration(seconds: 1), () {
final _ = 100 ~/ 0;
});
print('Everything is fine!');
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} finally {
print('Clean-up done');
}
Output:
Everything is fine!
Clean-up done
Unhandled exception:
IntegerDivisionByZeroException
#0 int.~/ (dart:core-patch/integers.dart:22:7)
#1 main.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/async_exceptions.dart:6:21)
#2 new Future.delayed.<anonymous closure> (dart:async/future.dart:325:39)
#3 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#4 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
#5 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#6 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
We’d like to handle the exception in the Future
delayed for one second. It’s natural that this code fails to catch the exception as the code in the try-catch
block just schedules the _computation_
(Futures callback/closure) to be invoked one second after try-catch
finishes its work.
It would look something like this on a timeline:
How to fix it? There are many ways of fixing it, for instance, by adding await instruction before the Future. It forces computation to be invoked inside the try-catch
and thus the try
block will be able to catch and handle the exception successfully.
try {
await Future.delayed(Duration(seconds: 1), () {
final _ = 100 ~/ 0;
});
print('Everything is fine!');
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} finally {
print('Clean-up done');
}
Output:
Cant divide to zero
Clean-up done
Now it works fine, we managed to catch the exception and make a clean-up after it.
We can represent the new code like this:
What if we have a chain of futures? No problem at all, try-catch
will catch exceptions in all of them as long as we insert await
before the first of them.
try {
await Future.delayed(Duration(seconds: 1), () {
print('Its ok so far');
}).then((value) {
final _ = 100 ~/ 0;
});
print('Everything is fine!');
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} finally {
print('Clean-up done');
}
Output:
Its ok so far
Cant divide to zero
Clean-up done
If you don’t like the above way of catching the exceptions you can use a dot syntax and catchError
function.
await Future.delayed(Duration(seconds: 1), () {
print('Its ok so far');
}).then((value) {
final _ = 100 ~/ 0;
}).catchError((error) {
print('Cant divide to zero');
}).whenComplete(() {
print('Clean-up done');
});
Output:
Its ok so far
Cant divide to zero
Clean-up done
In the above code, catchError
is an analog of catch
block and whenComplete
is an analog of finally
block. Function catchError
catches exceptions in all the code that located above it, so if you suspect that whenComplete
can throw an exception you can add one more catchError
after it.
await Future.delayed(Duration(seconds: 1), () {
print('Its ok so far');
}).then((value) {
final _ = 100 ~/ 0;
}).catchError((error) {
print('Cant divide to zero');
}).whenComplete(() {
print('Clean-up done');
throw 'ss';
}).catchError((error) {
print('Oh, error in the finally block');
});
Output:
Its ok so far
Cant divide to zero
Clean-up done
Oh, error in the finally block
You can use exceptions matching using test parameter of a catchError function.
await Future.delayed(Duration(seconds: 1), () {
print('Its fie so far');
}).then((value) {
print('Then started');
throw 'random exception';
}).catchError((error) {
print('Cant divide to zero');
}, test: (e) => e is IntegerDivisionByZeroException).catchError((error) {
print('All other exceptions: $error');
}, test: (e) => true).whenComplete(() {
print('Clean-up done');
});
Output:
Its fie so far
Then started
All other exceptions: random exception
Clean-up done
Looks not super readable, right? That’s why a Dart code style recommends using await
(code style guide)and thus try-catch
in such cases. It could look like this:
try {
await Future.delayed(Duration(seconds: 1), () {
print('Its fie so far');
});
await Future(() {
print('Then started');
throw 'random exception';
});
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} catch (e, s) {
print('All other exceptions: $e');
} finally {
print('Clean-up done');
}
Output:
Its fie so far
Then started
All other exceptions: random exception
Clean-up done
From my personal point of view, it’s much clearer and more readable.
Additionally, It has one more advantage — it can handle both synchronous and asynchronous code, so if you need to make some calculations between two asynchronous calls you don’t have to create additional synchronous Future (as you would have done in the case of dot syntax), just add this code into try block.
Ooook, this was easy, right? What about this piece of code?
try {
Timer.run(() {
print('Timer runs.');
throw Exception('[Timer] Bad thing happens!');
});
print('Everything is fine!');
} on IntegerDivisionByZeroException {
print('Cant divide to zero');
} finally {
print('Clean-up done');
}
Everything is fine!
Clean-up done
Timer runs.
Unhandled exception:
Exception: [Timer] Bad thing happens!
#0 main.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/async_exceptions.dart:7:7)
#1 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#2 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
#3 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
:( (smile with crying). Yeah, bad things happen in timers too. And the above code is not good to deal with those bad things as again timer’s callback will be invoked not in the try-catch
block and we can’t fix it with await instruction as we can’t wait on Timer.
An obvious solution is to add try-catch
inside the timer callback but if we don’t want to contaminate the callbacks with exceptions handling or there is a much complicated asynchronous logic inside the timer’s callback and we want to handle exceptions from that logic in the place where we run timer?
Zones and runZonedGuarded
runZonedGuarded
can help you with this and with more complicated cases too!
Zones
(dart help) is a concept in Dart that helps to divide your code into error free zones where any exceptions in any pieces of asynchronous code will be handled in one place: zone’s uncaught error handler.
How does it work? Each callback or computation in timers, microtasks, and Futures
is being wrapped into a special handler in the Zone
. The handler wraps the callback in try-catch
and if catch is being triggered Zone
invokes an uncaught error handler where you can handle those errors.
Each piece of code is being run in some Zone
, even you don’t create your custom Zone
it would be invoked in the Root Zone
which means we are safe anyway!
Let’s see how we can fix the problem with the timer:
runZonedGuarded((){
Timer.run(() {
print('Timer runs.');
throw Exception('[Timer] Bad thing happens!');
});
print('Everything is fine!');
}, (e,s) {
print('Exception handled $e, \n$s');
});
Output:
Everything is fine!
Timer runs.
Exception handled Exception: [Timer] Bad thing happens!,
#0 main.<anonymous closure>.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/async_exceptions.dart:7:7)
#1 _rootRun (dart:async/zone.dart:1182:47)
#2 _CustomZone.run (dart:async/zone.dart:1093:19)
#3 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
#4 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
#5 _rootRun (dart:async/zone.dart:1190:13)
#6 _CustomZone.run (dart:async/zone.dart:1093:19)
#7 _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1021:23)
#8 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#9 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
#10 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#11 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
Easy! Just wrap your code into runZonedGuarded
and add an error handler. No exception can squeeze through this construct… until we will not throw the exception in the error handler itself as the error handler is being invoked in the parent zone, so, be careful.
You can notice that the stack trace becomes longer as now async callbacks are wrapped into our _CustomZone
handlers.
Zones
is a very powerful tool that can help you to isolate some parts of your complex asynchronous logic from the other parts of the app. As we will see later it’s also rather useful when you use streams and for handling unhandled exceptions in Flutter apps.
If we would try to represent handling of exceptions is zones it could look like this:
In the about image we can see that all the asynchronous callbacks are covered by the zone’s exception handling if we schedule those callbacks inside the zone.
Additionally, Zones
helps you to intercept the creation of timers and print operators, and, keep some parameters in the zone’s context (it’s a dictionary that you can pass to the zone while you creating it). All those advanced things are being used in tests mainly, as for my knowledge.
This is an example of how we can use nested zones and zone’s context parameters — I added the parameter ZoneName
where I write Zone
name and display it in prints.
print('Current zone start in: ${Zone.current.toString()}');
runZonedGuarded(() {
print(
'Current zone inside runZoned: ${Zone.current.toString()} with name ${Zone.current['ZoneName']}');
Timer.run(() {
print('Timer runs.');
throw Exception('[Timer] Bad thing happens!');
});
runZonedGuarded(
() {
print(
'Current zone (1) inside runZoned: ${Zone.current.toString()} with name ${Zone.current['ZoneName']}');
Timer.run(() {
print('Timer 1 runs. ');
throw Exception('[Timer 1] Bad thing happens!');
});
},
(e, s) {
print(
'Current zone (1) inside catch: ${Zone.current.toString()} with name ${Zone.current['ZoneName']}');
print('Exception handled (1) $e, \n$s');
},
zoneValues: {'ZoneName': 'Second zone'},
);
print('Everything is fine!');
}, (e, s) {
print(
'Current zone inside catch: ${Zone.current.toString()} with name ${Zone.current['ZoneName']}');
print('Exception handled $e, \n$s');
}, zoneValues: {'ZoneName': 'First zone'});
Output:
Current zone start in: Instance of '_RootZone'
Current zone inside runZoned: Instance of '_CustomZone' with name First zone
Current zone (1) inside runZoned: Instance of '_CustomZone' with name Second zone
Everything is fine!
Timer runs.
Current zone inside catch: Instance of '_RootZone' with name null
Exception handled Exception: [Timer] Bad thing happens!,
#0 main.<anonymous closure>.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/async_exceptions.dart:10:7)
#1 _rootRun (dart:async/zone.dart:1182:47)
#2 _CustomZone.run (dart:async/zone.dart:1093:19)
#3 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
#4 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
#5 _rootRun (dart:async/zone.dart:1190:13)
#6 _CustomZone.run (dart:async/zone.dart:1093:19)
#7 _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1021:23)
#8 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#9 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
#10 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#11 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
Timer 1 runs.
Current zone (1) inside catch: Instance of '_CustomZone' with name First zone
Exception handled (1) Exception: [Timer 1] Bad thing happens!,
#0 main.<anonymous closure>.<anonymous closure>.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/async_exceptions.dart:18:11)
#1 _rootRun (dart:async/zone.dart:1182:47)
#2 _CustomZone.run (dart:async/zone.dart:1093:19)
#3 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
#4 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
#5 _rootRun (dart:async/zone.dart:1190:13)
#6 _CustomZone.run (dart:async/zone.dart:1093:19)
#7 _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1021:23)
#8 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#9 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
#10 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#11 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
From the above example, you can see that it’s rather convenient to use nested zones to divide your code into error free zones and, probably, use zone’s context to keep so useful info for logging purposes.
Error handling in streams
I promised to reveal how zones can help with handling exceptions when we use streams.
Let’s consider the simple code where one piece of code emits some events to the stream and another one consumes them.
import 'dart:async';
void main(List<String> arguments) async {
startListener(getStream());
}
Stream<int> getStream() {
final intsController = StreamController<int>.broadcast();
int i = 42;
Timer.periodic(Duration(seconds: 1), (timer) {
i += 1;
if (i == 46) throw '[Stream] Bad thing happens!';
intsController.add(i);
});
return intsController.stream;
}
void startListener(Stream<int> ints) {
ints.listen((event) {
print(event);
if (event == 44) {
throw '[Listener] Bad thing happens!';
}
});
}
Output:
43
Unhandled exception:
[Listener] Bad thing happens!
#0 startListener.<anonymous closure> (file:///Users/artemgoncharov/Downloads/dart_sample/bin/streams_exceptions.dart:23:7)
#1 _RootZone.runUnaryGuarded (dart:async/zone.dart:1384:10)
#2 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:357:11)
#3 _DelayedData.perform (dart:async/stream_impl.dart:611:14)
#4 _StreamImplEvents.handleNext (dart:async/stream_impl.dart:730:11)
#5 _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:687:7)
#6 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
#7 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
#8 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#9 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:404:11)
#10 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#11 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
44
I know this code is not perfect at all :) but it demonstrate the problem — we would have an unhandled exception in the listener. An obvious solution is to add try-catch inside the listener. And again, it could be hard because there could be multiple listeners and some asynchronous logic in place and we may want to handle all the exceptions from the listeners and the business logic in one place.
We wrap listeners creation into one zone and producers creation in another one, and, now we can handle all the exceptions from both sides and log them and/or recover in one place that is very convenient:
import 'dart:async';
void main(List<String> arguments) async {
Stream<int> stream;
runZonedGuarded(() {
stream = getStream();
}, (e, s) {
print('PRODUCER ZONE Exception handled: $e, $s');
});
runZonedGuarded(() {
startListener(stream);
}, (e, s) {
print('CONSUMER ZONE Exception handled: $e, $s');
});
}
Stream<int> getStream() {
final intsController = StreamController<int>.broadcast();
int i = 42;
Timer.periodic(Duration(seconds: 1), (timer) {
i += 1;
if (i == 46) throw '[Stream] Bad thing happens!';
intsController.add(i);
});
return intsController.stream;
}
void startListener(Stream<int> ints) {
ints.listen((event) {
print(event);
if (event == 44) {
throw '[Listener] Bad thing happens!';
}
});
Output:
43
44
CONSUMER ZONE Exception handled: [Listener] Bad thing happens!, #0 startListener.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/streams_exceptions.dart:35:7)
#1 _rootRunUnary (dart:async/zone.dart:1206:13)
#2 _CustomZone.runUnary (dart:async/zone.dart:1100:19)
#3 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1005:7)
#4 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:357:11)
#5 _DelayedData.perform (dart:async/stream_impl.dart:611:14)
#6 _StreamImplEvents.handleNext (dart:async/stream_impl.dart:730:11)
#7 _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:687:7)
#8 _rootRun (dart:async/zone.dart:1182:47)
#9 _CustomZone.run (dart:async/zone.dart:1093:19)
#10 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
#11 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
#12 _rootRun (dart:async/zone.dart:1190:13)
#13 _CustomZone.run (dart:async/zone.dart:1093:19)
#14 _CustomZone.runGuarded (dart:async/zone.dart:997:7)
#15 _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1037:23)
#16 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
#17 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)
#18 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#19 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:404:11)
#20 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#21 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
45
PRODUCER ZONE Exception handled: [Stream] Bad thing happens!, #0 getStream.<anonymous closure> (file:///Users/user/Downloads/dart_sample/bin/streams_exceptions.dart:24:18)
#1 _rootRunUnary (dart:async/zone.dart:1198:47)
#2 _CustomZone.runUnary (dart:async/zone.dart:1100:19)
#3 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1005:7)
#4 _CustomZone.bindUnaryCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1042:26)
#5 _rootRunUnary (dart:async/zone.dart:1206:13)
#6 _CustomZone.runUnary (dart:async/zone.dart:1100:19)
#7 _CustomZone.bindUnaryCallback.<anonymous closure> (dart:async/zone.dart:1026:26)
#8 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:397:19)
#9 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:428:5)
#10 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)
Warning! I’m not saying we have to give up handling errors as close to the place where they can arise, I’m just saying that sometimes it’s very useful to wrap the complex features to zones in order to catch and properly log the errors that sometimes squeeze through all our downstream bastions.
On this optimistic note, I’m going to get into the last topic: Unhandled exceptions.
Unhandled exceptions
As we saw before all the unhandled exceptions are intercepted by the zone the code is being invoked in. So that if you’d like to log the unhandled exceptions in you flutter app you need just wrap your runApp
call into the runZoneGuarded
and add logging in the error handler. If you do something else in main function — create providers or singletons — you may want to wrap them to the runZonedGuarded
as well.
This helps you to be aware of any unhandled exceptions in your app.
You may argue that you can see all of them in the terminal when you debug your app. This is super convenient for debugging but it has two nuances:
- If you debug your app directly on a device (not on Simulator) you will not be seeing the exceptions in the asynchronous code (unless you use
runZonedGuarded
) - In release builds, I’m sure, you want to be aware of all the unhandled exceptions even if it seems that app is working fine. So that you need to add
runZonedGuarded
and log all the exceptions to your backend or Firebase/Sentry, or whatever system you use for remote logging
However, runZoneGuarded
can’t help you with the exceptions which were already handled by the Flutter framework.
All those exceptions can be logged using FlutterError.onError
callback. If you add logging to this callback then anytime Flutter catches an error with rendering or anything in widgets it will reach that callback and will be logged by you. Don’t forget to WidgetsFlutterBinding.ensureInitialized()
before adding the onError
callback.
void main() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (FlutterErrorDetails errorDetails) {
print("onError Exception: $errorDetails was caught by Flutter framework - redirect to Sentry or Firebase.");
};
runZonedGuarded(() {
createProviders();
runApp(MyApp());
}, (e, s) {
print(
"Synchronous or Asynchronous Exception: $e (stack $s) was caught in our custom zone - redirect to Sentry or Firebase.");
});
}
Wrap up
It seems that brevity is not one my features but I hope the article was still useful and can be of help for you.
Additionally, if you still haven’t forgotten about the 3 things from the title, here they are:
- There are many ways of dealing with exceptions in different languages, try-catch is not the only one, and, it depends on your design what exception handling method you want to use — in Dart you can implement many of them
- Use runZoneGuarded to catch exceptions in the complex asynchronous code, streams, and in a whole Flutter app too
- Unhandled exceptions in asynchronous code can be silently swallowed (not printed in the terminal) if you debug your app on the real device
This article was written after my presentation (YouTube link) in Singapore Flutter Meetup which you can find here: https://www.meetup.com/Singapore-Flutter-Meetup/events/276139032/
List of sources
https://api.flutter.dev
https://dart.dev/get-dart
https://github.com/flutter/flutter
Top comments (0)