Skip to content
JoanZapata edited this page Dec 19, 2014 · 6 revisions

Usecase

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.

Solution concept

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.

Usage of @ErrorManagement

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. :))

Want more? See what's [behind the scenes](Behind The Scenes).

Learn

  1. Learn the basics.
  2. How to [enhance your service](Enhance Service).
  3. Take advantage of [caching](Caching Patterns).
  4. How to [handle exceptions](Handle Exceptions).
  5. Want more? See what's [behind the scenes](Behind The Scenes).
Clone this wiki locally