Error handling: Make your Exceptions nicer

June 28, 2018

hero

Error handling is hard and every developer I know doesn’t like to do it. It’s almost always the last thing to be done in a new project. You have to handle different kinds of errors — from no network connectivity, to servers being down, to user-made validation errors (either local or server-side) all of which lead to a cumbersome correcting process for all parts of the app. I would like to show you our approach to this.

Our setup is a common one — for the API layer we use Retrofit with OkHttp client and the data is read reactively via RxJava2. The architecture of our apps is MVVM (but it doesn’t matter so much since this can be applied even for MVP). Our ViewModel holds an instance of some Repository and this repository is responsible for calling Retrofit API — simple enough.

Example of Error Handling

Let’s model our example on a simple login request — email, password … nothing more.

Imagine a situation where we have some server that returns a 401 status code on invalid credentials, a 400 on validation errors and a 200 with a user object when all goes smoothly. 400 requests have error bodies with additional info of which field has what validation error, like:

{
 "fields": [
   {
     "field": "email",
     "error": "invalid_email"
   }
 ]
}

And now we want to handle all possible errors that can happen when making a request to this api.

So, here’s how some general first-thought error handling for a Login screen could look:

// 400 error body entities
class FieldError(val field: String, val error: String)

class ValidationErrorResponse(val fields: List<FieldError>)

// repository reading data from api
class Repository(val api: Api) {
 fun login(email: String, password: String): Single<User> = api.login(email, password)
}

// representation of login result - either OK or Error states
sealed class LoginResult {

 object LoginOk : LoginResult()
 data class LoginError(val error: Throwable) : LoginResult()
}

// viewModel making request and converting it to LoginResult and
// providing observable of this result
class LoginViewModel(val repository: Repository) {
 private val loginResultSubject = BehaviorSubject.create<LoginResult>()

 fun observeLoginResult(): Observable<LoginResult> = loginResultSubject

 fun login(email: String, password: String) {
   repository.login(email, password)
       .subscribeOn(Schedulers.newThread())
       .subscribe({
         loginResultSubject.onNext(LoginResult.LoginOk)
       }, {
         loginResultSubject.onNext(LoginResult.LoginError(it))
       })
 }
}

// fragment (or activity) that is View layer that pass actions (login button click) to
// viewModel and observes viewModel state
class LoginFragment : Fragment {

 val viewModel: LoginViewModel
 var disposables: CompositeDisposable

 fun onViewCreated() {
   btnLogin.onClick {
     viewModel.login(editEmail.text, editPassword.text)
   }

   disposables += viewModel.observeLoginResult()
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe {
         when (it) {
           is LoginResult.LoginOk -> finishLogin()
           is LoginResult.LoginError -> {
             when (it.error) {
               is IOException -> showNoConnectinError()
               is HttpException -> {
                 when (it.error.code()) {
                   401 -> showInvalidCredentialsError()
                   400 -> {
                     val errorResponse: ValidationErrorResponse = it.error.bodyToValidationError()
                     errorResponse.fields.forEach {
                       when (it.field) {
                         "email" -> {
                           when (it.error) {
                             "invalid_email" -> showInvalidEmailError()
                           }
                         }
                         "password" -> {
                           when (it.error) {
                             "short_password" -> showShortPasswordError()
                             "weak_password" -> showWeakPasswordError()
                           }
                         }
                       }
                     }
                   }
                   else -> showGeneralServerError()
                 }
               }
               else -> unexpectedError() // crash?
             }
           }
         }
       }
 }
}

Phew, that was a lot of code. A lot of ugly code. First, to clarify some things — In this scenario it looks as though the Repository layer is useless but in the real world, it contains more logic and as we will see, it will be useful for us too.

So, the error handling is not very nice .. and imagine having to do this to every api request in all places in the app. ugh ..

Other than it not being very nice, it’s also not clean — the logic behind error processing, error body format, and all other possible situations that can arise from this request, does not in any way belong to View layer. Where does it belong? ViewModel? Nope. ViewModel is just a link between Model (business logic) and View. In our case, Model is Repository so that’s the piece that should contain this logic. So let’s make it a little nicer.

First let’s introduce some more classes representing possible errors 🤓:

class NoInternetConnectionException() : Exception()
class GeneralServerException(val code: Int) : Exception()
class UnexpectedException(val originalException: Exception) : Exception()
class InvalidCredentialsException : Exception()
class ValidationException(val errors: Map<String, List<String>>) : Exception()

and now let’s move the code from the View to Model:

class Repository(val api: Api) {
 fun login(email: String, password: String): Single<User> {
   return api.login(email, password)
       .onErrorResumeNext { error: Throwable ->
         val exception = when (error) {
           is HttpException -> {
             when (error.code()) {
               401 -> InvalidCredentialsException()
               400 -> {
                 val errorResponse: ValidationErrorResponse = error.bodyToValidationError()
                 ValidationException(errorResponse.fields.groupBy({ it.error }, { it.field }))
               }
               else -> GeneralServerException()
             }
           }
           is IOException -> NoInternetConnectionException()
           else -> UnexpectedException(error)
         }
         Single.error(exception)
       }
 }
}

We are using the operator onErrorResumeNext from RxJava that maps errors to different streams. We just map it again to error stream but with mapped Retrofit Exceptions to our domain exceptions. Now the error handling in view model looks a lot nicer:

fun onViewCreated() {
 disposables += viewModel.observeLoginResult()
   .observeOnMainThread()
   .subscribe {
     when (it) {
       is LoginResult.LoginOk -> finishLogin()
       is LoginResult.LoginError -> {
         when (it.error) {
           is NoInternetConnectionException -> showNoConnectionError()
           is InvalidCredentialsException -> showInvalidCredentialsError()
           is GeneralServerException -> showGeneralServerError()
           is ValidationException -> showValidationErrors(it.error.errors)
           is UnexpectedException -> unexpectedError() // crash?
         }
       }
     }
   }
}

Beautiful. Some of the stuff can be generalized with a little help from Kotlin extensions.

First, the error mapping in Repository still needs to be done for every request so we can extract the common part elsewhere.

Let’s create an extension called mapApiExceptions on Single that will do the common stuff:

fun <T> Single<T>.mapApiExceptions(errorCodeMapper: ((HttpException) -> Exception?)? = null): Single<T> {
 return onErrorResumeNext { err: Throwable ->
   val exception = when (err) {
     is HttpException -> {
       errorCodeMapper?.invoke(err) ?: GeneralServerException(err.code())
     }
     is IOException -> NoInternetConnectionException()
     else -> UnexpectedException(err)
   }
   Single.error<T>(exception)
 }
}

It accepts an optional mapper function from HttpException to other Exceptions. If this function is null or if it returns null, map the results to GeneralServerException.

Now our repository code changes to:

class Repository(val api: Api) {
    fun login(email: String, password: String): Single<User> {
      return api.login(email, password)
          .mapApiExceptions {
            when (it.code()) {
              401 -> InvalidCredentialsException()
              400 -> {
                val errorResponse: ValidationErrorResponse = it.bodyToValidationError()
                ValidationException(errorResponse.fields.groupBy({ it.error }, { it.field }))
              }
              else -> null
            }
          }
    }
   }

View layer can also be optimized, GeneralServerException, NoInternetException and UnexpectedException will probably be handled the same in all places, so abstract it:

fun Fragment.handleErrors(error:Throwable) {
 when (error) {
   is NoInternetConnectionException -> snackbar(view, R.string.general_no_connection_error)
   is GeneralServerException -> snackbar(view, R.string.general_server_error)
   else -> throw error
 }
}

class LoginFragment : Fragment {
 val viewModel: LoginViewModel
 var disposables: CompositeDisposable

 fun onViewCreated() {
   disposables += viewModel.observeLoginResult()
       .observeOnMainThread()
       .subscribe {
         when (it) {
           is LoginResult.LoginOk -> finishLogin()
           is LoginResult.LoginError -> {
             when (it.error) {
               is InvalidCredentialsException -> showInvalidCredentialsError()
               is ValidationException -> showValidationErrors(it.error.errors)
               else -> handleErrors(it.error)
             }
           }
         }
       }
 }
}

If we compare our original code to the current one, one has to smile at how beautiful it is. It’s pretty easy to process another api request in a different part of the app.

Another improvement that can be done is to map exceptions to our domain exceptions automatically in a OkHttp Interceptor/Retrofit CallAdapter, but that’s a topic for a different blog 🙃.

Disclaimer: The code in this blogpost can be taken as a pseudocode, there are a lot of not implemented methods that were not crucial for this blog to be mentioned.