In Dart, the dart:async library provides powerful tools like Futures and Streams to handle asynchronous operations — and proper error handling with them is crucial for building reliable applications.
When working with Futures, I usually handle errors using either .catchError() or try-catch inside an async function. For example, in one of my Flutter apps, I was calling an API using http.get, and I wanted to gracefully handle network errors without crashing the app:
Future fetchUserData() async {
try {
final response = await http.get(Uri.parse('https://api.example.com/user'));
if (response.statusCode == 200) {
print('User data fetched successfully');
} else {
throw Exception('Server returned error: ${response.statusCode}');
}
} catch (error) {
print('Error fetching user data: $error');
}
}
Here, try-catch helps me catch both synchronous and asynchronous exceptions in a single block. Alternatively, if I’m chaining Futures, I can use .catchError() like this:
getUserData()
.then((data) => print(data))
.catchError((error) => print('Error: $error'));
This approach is useful for short async flows, but in larger functions I prefer async/await with try-catch for better readability.
Now, for Streams, error handling works a bit differently because streams can emit multiple events over time — including data, errors, and completion signals. The key is to use the onError parameter or handleError() method when listening to a stream.
For example:
stream.listen(
(data) => print('Received data: $data'),
onError: (error) => print('Stream error: $error'),
onDone: () => print('Stream closed'),
);
In one of my projects that streamed live sensor data from a Bluetooth device, I used this approach to handle cases when the connection dropped. Whenever the stream threw an error, I displayed a “Connection lost” message and attempted a reconnect.
You can also use the handleError() operator in a stream chain:
stream
.handleError((error) {
print('Handled error inside stream: $error');
})
.listen((data) => print('Data: $data'));
A challenge I faced was when errors occurred inside a StreamTransformer or a broadcast stream — sometimes they didn’t propagate as expected. To fix that, I made sure to rethrow or forward the error downstream properly using sink.addError(error) within the transformer logic.
The limitation with stream error handling is that once an unhandled error occurs in a single-subscription stream, the stream closes automatically. If I need resilience (like reconnect logic), I usually wrap the stream or re-subscribe.
As an alternative, for complex scenarios like multiple async sources or retry strategies, I’ve used libraries like RxDart, which offer operators like onErrorResumeNext or retryWhen — giving more granular control over error handling and recovery.
So in short, with dart:async, I rely on:
try-catchfor Futures (especially with async/await),onErrororhandleErrorfor Streams,
and make sure to always handle errors close to the source to keep the async flow robust and predictable.
