I upgraded Flutter from version 2.0.2 to version 2.2.2 and now the custom exceptions that are thrown from a Future function are not being catch.
For example, I got this Future function, where I call another Future that does a server request and returns back the response or throws a custom exception (ApiException) in case of error:
static Future<bool> signUpCustomerRequest(Map<String, dynamic> params) async {
try {
// Here we call this Future function that will do a request to server API.
dynamic _response = await _provider.signUpCustomer(params);
if (_response != null) {
updateUserData(_response);
return true;
}
return false;
} on ApiException catch(ae) {
// This custom exception is not being catch
ae.printDetails();
rethrow;
} catch(e) {
// This catch is working and the print below shows that e is Instance of 'ApiException'
print("ERROR signUpCustomerRequest: $e");
rethrow;
} finally {
}
}
And this is the Future function that does the request to server and throws the ApiException:
Future<User?> signUpCustomer(Map<String, dynamic> params) async {
// POST request to server
var _response = await _requestPOST(
needsAuth: false,
path: routes["signup_client"],
formData: params,
);
// Here we check the response...
var _rc = _response["rc"];
switch(_rc) {
case 0:
if (_response["data"] != null) {
User user = User.fromJson(_response["data"]["user"]);
return user;
}
return null;
default:
print("here default: $_rc");
// And here we have the throw of the custom exception (ApiException)
throw ApiException(getRCMessage(_rc), _rc);
}
}
Before upgrading to Flutter 2.2.2 the catch of custom exceptions worked perfectly. Did something change on this Flutter version? Am I doing something wrong?
Thanks!
I was able to reproduce your bug with the following code:
class ApiException implements Exception {
void printDetails() {
print("ApiException was caught");
}
}
Future<void> doSomething() async {
await Future.delayed(Duration(seconds: 1));
throw ApiException();
}
void main() async {
try {
await doSomething();
} on ApiException catch (ae) {
ae.printDetails();
} catch (e) {
print("Uncaught error: $e"); // This line is printed
}
}
There's an open issue on the dart sdk, which I think might be related, though I'm not sure: https://github.com/dart-lang/sdk/issues/45952.
In any case, I was able to correct the error by returning a Future.error, instead of throwing the error directly:
class ApiException implements Exception {
void printDetails() {
print("ApiException was caught"); // This line is printed
}
}
Future<void> doSomething() async {
await Future.delayed(Duration(seconds: 1));
return Future.error(ApiException());
}
void main() async {
try {
await doSomething();
} on ApiException catch (ae) {
ae.printDetails();
} catch (e) {
print("Uncaught error: $e");
}
}
Related
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;
}
}
I have an exception that gets thrown multiple times before getting handled in my code, (the same error object gets thrown 4 times in the tree).
During each throw , flutter appends the word Exception: to my error object, so the final shape of the message is:
Exception: Exception: Exception: Exception: SocketException: OS Error: Connection refused, errno = 111, address = 10.12.7.15, port = 39682
And this is an example of how I handle exceptions:
getSomeData () async {
try {
await myFunction3();
} catch (e) {
throw Exception(e);
}
}
/*****************************************/
Future<void> myFunction3 () async {
try {
await myFunction2();
} catch (e) {
throw Exception(e);
}
}
/*****************************************/
Future<void> myFunction2 () async {
try {
await myFunction1();
} catch (e) {
throw Exception(e);
}
}
/*****************************************/
Future<void> myFunction1() async {
try {
await dio.get(/*some parameters*/);
}
on DioError catch (e) {
throw Exception(e.error);
}
catch (e) {
throw Exception(e);
}
}
I want to throw the Exception() in a right way so the repetition of word Exception: doesn't appear anymore in my error string.
Instead of throwing a new exception, you could rethrow the catched one:
Future<void> myFunction1() async {
try {
await dio.get(/*some parameters*/);
}
on DioError catch (e) {
// do something
rethrow;
}
catch (e) {
// do something
rethrow;
}
}
See more: https://dart.dev/guides/language/effective-dart/usage#do-use-rethrow-to-rethrow-a-caught-exception
I am trying to create a test for timeout using Dio, I expect get DioError with type CONNECT_TIMEOUT then throw a custom exception
My test I mock Dio with Mockito and try throw DioError
test(
'Should throw [ConnectionTimeOutException] when reach timeout',
() async {
//arange
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
when(mockDio.post(paths.login, data: tParams.toJson())).thenThrow(
(_) async => DioError(type: DioErrorType.CONNECT_TIMEOUT));
//act
final call = loginDataSource.login;
//assert
expect(() => call(params: tParams),
throwsA(TypeMatcher<ConnectTimeOutException>()));
},
);
My data source class:
class LoginDataSourceImpl implements LoginDataSource {
final Dio dio;
final NetworkInfo networkInfo;
LoginDataSourceImpl({#required this.dio, #required this.networkInfo});
#override
Future<CredencialModel> login({#required Params params}) async {
if (!await networkInfo.isConnected) {
throw NoNetworkException();
}
try {
final response = await dio.post(paths.login, data: params.toJson());
if (response.statusCode == 200) {
return CredencialModel.fromJson(response.data);
} else if (response.statusCode == 400) {
final error = ResponseError.fromJson(response.data);
switch (error.error) {
case 'invalid_request':
throw InvalidRequestException();
break;
case 'invalid_device':
throw InvalidDeviceException();
break;
case 'invalid_user_credentials':
throw InvalidUserCredentialException();
break;
case 'user_disabled':
throw UserDisableException();
default:
throw UnknowException();
}
} else if (response.statusCode == 500) {
throw ServerException();
} else {
throw UnknowException();
}
} on DioError catch (e) {
if (e.type == DioErrorType.CONNECT_TIMEOUT) {
throw ConnectTimeOutException();
} else if (e.type == DioErrorType.RECEIVE_TIMEOUT) {
} else {
throw UnknowException();
}
}
}
}
The result of the test is:
Expected: throws <Instance of 'ConnectTimeOutException'>
Actual: <Closure: () => Future<CredencialModel>>
Which: threw <Closure: (dynamic) => DioError>
stack package:mockito/src/mock.dart 385:7
How can i solve this issue and create a Timeout test with Dio?
There are a couple of problems with your approach.
First, you are testing an async method but you are not awaiting it. This is going to cause the raw Future object to be returned to the expect function which is going to consider it a successful call, even if the future ends up throwing an error. You will need to await your call, although doing so as a closure passed to expect is awkward. I would suggest wrapping the asynchronous call in a try/catch instead.
Second, you are providing a closure to Mockito's thenThrow method. This method takes whatever you give to it and uses it as the actual thrown value, so it isn't going to call the closure you passed to it - it will just throw it as-is.
Fixing these both, you end up with this:
test(
'Should throw [ConnectionTimeOutException] when reach timeout',
() async {
// arrange
when(mockNetworkInfo.isConnected)
.thenAnswer(true);
when(mockDio.post(paths.login, data: tParams.toJson()))
.thenThrow(DioError(type: DioErrorType.CONNECT_TIMEOUT));
// act
final call = loginDataSource.login;
// assert
try {
await call(params: tParams);
} catch(e) {
expect(e, isInstanceOf<ConnectTimeOutException>());
}
},
);
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.
I'm new to Flutter & Dart, trying to complete my first app.
I can't catch (with try-catch block) http.get SocketException (which happens when you call API and WiFi turned off)
I tried everything on the internet without luck, I even tried (Dio) package to catch this exception, but no success.
How to reproduce: use bottom code...turn off phone's WiFi...call API...now the app crashes with (SocketException) in your IDE.
Image: https://imgur.com/bA0rKEN
here is my simple code (updated)
RaisedButton(
child: Text("Call API"),
onPressed: () async {
try {
http.Response response = await getLoginResponse();
//do something with response
print(response.body);
} catch (e) {
print("Button onPressed Error: " + e.toString());
}
},
)
//---------------------------------------------------------------
Future<http.Response> getLoginResponse() {
return http.get(loginUrl).timeout(Duration(seconds: 10))
.then((response) {
return response;
}, onError: (e) {
print("onError: " + e.toString());
}).catchError((err) {
print("catchError: " + err.toString());
return null;
});
}
You can catch several types of errors and handle each one separately
Example:
import 'dart:io' as Io;
http.Client client = http.Client();
try {
response = await client.get(url).timeout(new Duration(seconds: 10));
} on Io.SocketException catch (_) {
throw Exception('Not connected. Failed to load data');
} on TimeoutException catch (_) {
throw Exception('Not connected. TimeOut Exception');
} catch (e) {
// Default error handling;
}
if you want to get catch in RaisedButton's try-catch block, instead of return null in getLoginInfo() methods, you must return an Exception like this:
Future<List<LoginObject>> getLoginInfo() async {
try {
List<LoginObject> loginObjectList = List<LoginObject>();
http.Response loginResponse =
await http.get(loginUrl).timeout(Duration(seconds: 10));
if (loginResponse.statusCode == 200) {
loginObjectList = loginObjectFromJson(loginResponse.body);
return loginObjectList;
} else {
throw Exception('Authentication Error');
}
} catch (e) {
print("Error: " + e.toString());
return throw Exception('Connection Error');;
}
}
Note: If you want to handle each one of error response, you can create an custom ErrorModelClass and handle error state with it and finally return your ErrorModelClass.
catch (error) {
print(error);
throw error is HttpResponseError ? error : HttpResponseError(0,"error connection");
HttpResponseError is my custom model class.