-
Notifications
You must be signed in to change notification settings - Fork 37
Handle Exceptions
We'll take the following AsyncService as an example.
@AsyncService
public class MyService {
...
/** Register a new user with the given username */
User registerUser(String username){
restClient.registerUser(username);
}
}
Now, let's say the server reject the call with a status code 409 (conflict) if the provided username is already taken. In our code, it means restClient.registerUser(username)
can throw an HttpClientErrorException
. With the default Android behavior, this will cause the app to crash.
Let's rely on event once again. The global pattern to manage these kinds of errors would be the following:
@AsyncService
public class MyService implements BaseService {
...
User registerUser(String username){
try {
restClient.registerUser(username);
} catch (HttpClientErrorException e){
/*
Test status code and send an error message
with appropriate name and information.
*/
if (e.getStatusCode().value() == 409){
sendMessage(new UsernameTakenError(e, username));
return null;
} else if (e.getStatusCode().value == 400){
sendMessage(new IllegalUsernameError(e, username));
return null;
}
// Let it go for now
throw e;
}
}
}
On the Activity
side, it's cool. The code is nice, short and strongly-typed.
@OnMessage
void onUsernameTaken(UsernameTakenError e) {
// Do something with e.getUsername()...
}
But the service's code is now very ugly with try..catch
and if..else
blocks.
AsyncService generalizes the above pattern like this:
@AsyncService(errorMapper = StatusCodeMapper.class)
public class MyService {
...
@ErrorManagement({
@On(code = 409, send = UsernameTakenError.class),
@On(code = 400, send = IllegalUsernameError.class)})
User registerUser(String username){
restClient.registerUser(username);
}
}
That's a lot better right? And this is strictly equivalent to the previous solution.
You may have noticed that in the later, we were constructing a new UsernameTakenError
using the username. So how does that work now? Let's look at UsernameTakenError
:
public class UsernameTakenError extends ErrorMessage {
public UsernameTakenError(Throwable throwable,
@ThrowerParam("username") String username) {
super(throwable);
...
}
...
}
This code speaks from itself: the UsernameTakenError
is constructed using the caller method parameters. And no worry, if you try to declare UsernameTakenError
on a method with no "username" parameter, it will throw at compile time. No surprise in production.
The only boilerplate code needed is the StatusCodeMapper
declared at the @AsyncService
annotation level. It makes a mapping between the thrown exception and an integer.
public class StatusCodeMapper implements ErrorMapper {
@Override
public int mapError(Throwable throwable) {
if (throwable instanceof HttpStatusCodeException)
return ((HttpStatusCodeException) throwable).getStatusCode().value();
else return SKIP;
}
}
Why do we have to use an integer? Firstly, because it has been designed with HTTP status codes in mind, and it covers most cases. Secondly, it's a lot easier to manage internally. :))
Created by Joan Zapata.
- Learn the basics.
- How to [enhance your service](Enhance Service).
- Take advantage of [caching](Caching Patterns).
- How to [handle exceptions](Handle Exceptions).
- Want more? See what's [behind the scenes](Behind The Scenes).