Home Android & Kotlin Books Functional Programming in Kotlin by Tutorials

14
Error Handling With Functional Programming Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

In the first two sections of this book, you learned everything you need to know about pure functions. You learned that a pure function has a body that’s referentially transparent and, maybe more importantly, doesn’t have side effects. A side effect is a “disturbance” of the world external to the function, making the function difficult to test. Exceptions are a very common example of a side effect. A function containing code that throws an exception isn’t pure and must be fixed if you want to use all the concepts you’ve learned so far.

In this chapter, you’ll learn how to handle exceptions — and errors in general — using a functional programming approach. You’ll start with a very simple example before exploring the solution Kotlin provides using the Result<T> data type. In particular, you’ll learn:

  • What exception handling means.
  • How to handle exceptions as side effects.
  • How to “purify” functions that throw exceptions.
  • How to use Optional<T> in functions with exceptions.
  • How to use Either<E, T> to handle exceptions in a functional way.
  • How to implement a ResultAp<E, T> monad.
  • How to compose functions that can throw exceptions.
  • What the Kotlin Result<T> data type is and how to use it in your application.

You’ll learn all this with a practical example that allows you to fetch and parse some content from the internet. This is the same code you’ll use in the RayTV app, which allows you to access data about your favorite TV shows.

Note: You’ll use the RayTV app in the following chapters as well.

You’ll also have the chance for some fun with a few exercises.

Exception handling

Note: Feel free to skip this section if you already have a clear understanding of what exceptions are and why they exist.

Every application is the implementation of a set of use cases in code. A use case is a way you describe how a specific user can interact with a system. Figure 14.1 is an example of a use case describing the scenario when a RayTV app user wants to search for their favorite TV show.

Figure 14.1: RayTV search use case
Figure 14.1: RayTV search use case

This use case diagram says two main things:

  1. Who can access the specific feature of the app. In this case, it’s the RayTV app user.
  2. What the RayTV app can do. In this case, it allows the user to search for a TV show by name.

Usually, you also describe the use case in words by writing a document like this:

Use Case: Search TV shows
Actors: RayTV app
Prerequisites: The user starts the RayTV app.
Steps:
1. The user selects the search option.
2. The app shows the keyboard.
3. The user inputs some text.
4. The user taps the search button.
5. The app accesses the search service and fetches the results.
6. The app displays the results in a list.
Final state: The app displays the results of the search.

It describes what the user can do and how the system reacts. This use case is very specific, though. It describes when everything works fine. You call this the happy path or principal use case. Many things can go wrong. For instance, a network error can happen, or the search simply doesn’t produce any results. In this case, you describe these situations using alternative use cases, like the one in Figure 14.2:

Figure 14.2: RayTV alternative use case
Figure 14.2: RayTV alternative use case

In this case, you describe the scenario as a use case that shows what happens when the happy path isn’t followed. In this case, the search doesn’t produce any result. Usually, you describe this scenario with a document like this:

Use Case: Handle empty results
Actors: RayTV app
Prerequisites: A TV show search produced no results.  
Steps:
1. The app displays a message that says the search has produced no results.
Final state: The app waits for action from the user.

This is all good, but what do use cases have to do with errors and exceptions? Well, exceptions aren’t always bad things. They’re just a way programming languages represent alternative scenarios. In general, you have three different types of situations:

  • Errors: These represent cases when something really wrong happens. They aren’t recoverable, and the developer should understand the cause and solve it. Typical examples are VirtualMachineException, IOError or AssertionError.
  • Checked exceptions: This is the type of exception that describes alternative use cases. If you have a connection error, you should somehow handle it. A user entering an invalid credit card number is a common occurrence, so it’s something your code should be prepared to handle. These are checked because in some programming languages, like Java, you have to explicitly declare them in the signature of the method that can throw them.
  • Unchecked exceptions: RuntimeException is another way to describe this type of exception because it can happen at runtime. A typical one is NullPointerException, which is thrown when you access a member of an object through a null reference. Although you can recover from a RuntimeException, they usually describe bugs you should eventually fix.

Kotlin doesn’t have checked exceptions, which means you don’t need to declare them as part of the function throwing them. This doesn’t mean you shouldn’t handle the exception. Kotlin represents them with classes extending Throwable and still provides you the try-catch-finally expression. So what’s the best way to handle exceptions controlling the flow of your program? Now that you have a solid functional programming background, you know that exceptions are side effects. How can you use what you’ve learned to handle exceptions elegantly and efficiently? Monads, of course! :]

Note: It’s interesting to remember how the type of the following expression is Nothing:

 throw IOException("Something bad happens")

Remembering the analogy between types and sets, you know that Nothing is a subtype of any other type. This means the value you return when you throw an exception has a type compatible with the return type of the function itself.

Handling exception strategies

In Chapter 13, “Understanding Monads”, you learned that functions throwing exceptions are impure. An exception changes the world outside the body of a function. In the same chapter, you also learned how to purify a function by changing the side effect to be part of the return type. The same works for exceptions, and you can achieve this using different data types depending on the strategy you want to adopt. Different strategies include:

Some preparation

Before describing the different ways of handling exceptions, it’s useful to look at some of the classes you find in the material for this chapter. These are the classes you initially find in the fp_kotlin_cap14 project, and you’ll see them again in the RayTV app in their final state. The RayTV app will allow you to search for a TV show and display some of the show’s information on the screen. This is basically the use case in Figure 14.1.

object TvShowFetcher {
  fun fetch(query: String): String {
    val encodedUrl = java.net.URLEncoder.encode(query, "utf-8")
    val localUrl =
      URL("https://api.tvmaze.com/search/shows?q=$encodedUrl")
    with(localUrl.openConnection() as HttpURLConnection) {
      requestMethod = "GET"
      val reader = inputStream.bufferedReader()
      return reader.lines().toArray().asSequence()
        .fold(StringBuilder()) { builder, line ->
          builder.append(line)
        }.toString()
    }
  }
}
object TvShowParser {
  /** Parses the input json */
  fun parse(json: String): List<ScoredShow> = Json {
    ignoreUnknownKeys = true
  }.decodeFromString<List<ScoredShow>>(
    ListSerializer(ScoredShow.serializer()), json
  )
}
Figure 14.3: TvShowFetcher and TvShowParser unit tests
Nosoni 63.5: RfGjijWiyvvak ihf DdXlobNuggom eqas jonkc

Handling errors with Optional<T>

The RayTV app sends a request to the server, gets some JSON back, parses it and displays the result to the user. This is the happy path, but many things can go wrong. Both TvShowFetcher::fetch and TvShowParser::parse can fail.

fun fetchTvShowOptional(
  query: String
): Optional<String> = try { // 1
  Optional.lift(TvShowFetcher.fetch(query))
} catch (ioe: IOException) {
  Optional.empty()
}

/** Invokes the parser returning an Optional */
fun parseTvShowString(
  json: String
): Optional<List<ScoredShow>> =
  try { // 2
    Optional.lift(TvShowParser.parse(json)) // 2
  } catch (e: SerializationException) {
    Optional.empty()
  }
fun <A, B> Optional<A>.flatMap(
  fn: Fun<A, Optional<B>>
): Optional<B> = when (this) {
  is None -> Optional.empty()
  is Some<A> -> {
    val res = fn(value)
    when (res) {
      is None -> Optional.empty()
      is Some<B> -> Optional.lift(res.value)
    }
  }
}
fun fetchAndParseTvShow(query: String) =
  fetchTvShowOptional(query)
    .flatMap(::parseTvShowString)
fun main() {
  fetchAndParseTvShow("Big Bang Theory") // 1
    .getOrDefault(emptyList<ScoredShow>()) pipe ::println // 2
}
[ScoredShow(score=1.2612172, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/original_untouched/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> ... </p>, language=English))]
[]

Handling errors with Either<E, T>

The Optional<T> data type is very useful, but it just tells you if you have a response; it doesn’t tell you what the error is if something goes wrong. In this case, a data type like Either<E, T> can help you. In lib, you’ll find Either.kt, which contains all the code you implemented in Chapter 9, “Data Types”. To see how to use it, just add the following code to ShowSearchService.kt, this time in the either sub-package:

fun fetchTvShowEither(
  query: String
): Either<IOException, String> = try {
  Either.right(TvShowFetcher.fetch(query)) // 1
} catch (ioe: IOException) {
  Either.left(ioe)
}

/** Invokes the parser returning an Optional */
fun parseTvShowEither(
  json: String
): Either<SerializationException, List<ScoredShow>> = try {
  Either.right(TvShowParser.parse(json)) // 2
} catch (e: SerializationException) {
  Either.left(e)
}
fun fetchAndParseTvShowEither(query: String) =
  fetchTvShowEither(query)
    .flatMap(::parseTvShowEither)
fun <A, B, D> Either<A, B>.flatMap(
  fn: (B) -> Either<A, D>
): Either<A, D> = when (this) {
  is Left<A> -> Either.left(left)
  is Right<B> -> {
    val result = fn(right)
    when (result) {
      is Left<A> -> Either.left(result.left)
      is Right<D> -> Either.right(result.right)
    }
  }
}
fun main() {
  fetchAndParseTvShowEither("Big Bang Theory")
    .leftMap {
      println("Error: $it")
    }
    .rightMap {
      println("Result: $it")
    }
}
Result: [ScoredShow(score=1.2627451, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/original_untouched/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>The Big Bang Theory</b> is a comedy...</p>, language=English))]
Error: java.net.UnknownHostException: api.tvmaze.com
fun parseTvShowEither(json: String): Either<SerializationException, List<ScoredShow>> =
  try {
    Either.right(TvShowParser.parse(json+"sabotage")) // HERE
  } catch (e: SerializationException) {
    Either.left(e)
  }
Error: kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 1589: Expected EOF after parsing, but had s instead
JSON input: .....ze.com/episodes/1646220}}}}]sabotage

Applicative functor

In the previous section, you learned how to handle exceptions using Optional<T> and Either<E, T> data types following a fail fast approach that stops the execution flow at the first exception. With Either<E, T>, you also get what’s wrong. In Chapter 9, “Data Types”, you learned that Either<A, B> is a bifunctor, which is an algebraic data type representing the addition of two types, A and B. In the previous use case, you used Left<E> to represent the error case and Right<T> for the success case.

sealed class ResultAp<out E : Throwable, out T> { // 1

  companion object {
    @JvmStatic
    fun <E : Throwable> error(
      error: E
    ): ResultAp<E, Nothing> = Error(error) // 2

    @JvmStatic
    fun <T> success(
      value: T
    ): ResultAp<Nothing, T> = Success(value) // 2
  }
}

data class Error<E : Throwable>(
  val error: E
) : ResultAp<E, Nothing>() // 3
data class Success<T>(
  val value: T
) : ResultAp<Nothing, T>() // 3

fun <E1 : Throwable, E2 : Throwable, T> ResultAp<E1, T>.errorMap(
  fl: (E1) -> E2
): ResultAp<E2, T> = when (this) { // 4
  is Error<E1> -> ResultAp.error(fl(error))
  is Success<T> -> this
}

fun <E : Throwable, T, R> ResultAp<E, T>.successMap(
  fr: (T) -> R
): ResultAp<E, R> = when (this) { // 5
  is Error<E> -> this
  is Success<T> -> ResultAp.success(fr(value))
}

ResultAp<E, T> as an applicative functor

One of the most important things you’ve learned in this book is that functions are values, which is why you can implement higher-order functions. This also means that T in ResultAp<E, T> can be a function type like (T) -> T. So, what would the meaning be of a function ap with the following signature?

fun <E : Throwable, T, R> ResultAp<E, T>.ap( // 1
  fn: ResultAp<E, (T) -> R> // 2
): ResultAp<E, R> { // 3
  // ...
}
fun <E : Throwable, T, R> ResultAp<E, T>.ap(
  fn: ResultAp<E, (T) -> R>
): ResultAp<E, R> = when (fn) {
  is Success<(T) -> R> -> successMap(fn.value)
  is Error<E> -> when (this) {
    is Success<T> -> Error(fn.error)
    is Error<E> -> Error(this.error)
  }
}
data class User(
  val id: Int,
  val name: String,
  val email: String
)
class ValidationException(msg: String) : Exception(msg) // 1

/** Name validation */
fun validateName(
  name: String
): ResultAp<ValidationException, String> =
  if (name.length > 4) {
    Success(name)
  } else {
    Error(ValidationException("Invalid name"))
  } // 2

/** Email validation */
fun validateEmail(
  email: String
): ResultAp<ValidationException, String> =
  if (email.contains("@")) {
    Success(email)
  } else {
    Error(ValidationException("Invalid email"))
  } // 3
fun main() {
  val userBuilder = ::User.curry() // 1
  val userApplicative = ResultAp.success(userBuilder) // 2
  val idAp = ResultAp.success(1) // 3
  validateEmail("max@maxcarli.it") // 6
    .ap(
      validateName("") // 5
        .ap(
          idAp.ap(userApplicative) // 4
        )
    )
    .errorMap { // 7
      println("Error: $it"); it
    }
    .successMap { // 7
      println("Success $it")
    }
}
Error: com.raywenderlich.fp.validation.ValidationException: Invalid Name
fun main() {
  // ...
  validateEmail("max@maxcarli.it")
    .ap(
      validateName("Massimo") // HERE
        .ap(idAp.ap(userApplicative))
    )
    // ...
}
Success User(id=1, name=Massimo, email=max@maxcarli.it)
infix fun <E : Throwable, A, B> ResultAp<E, (A) -> B>.appl(
  a: ResultAp<E, A>
) = a.ap(this)
fun main() {
  val userBuilder = ::User.curry()
  val userApplicative = ResultAp.success(userBuilder)
  val idAp = ResultAp.success(1)
  (userApplicative appl
      idAp appl
      validateName("Massimo") appl
      validateEmail("max@maxcarli.it"))
    .errorMap {
      println("Error: $it"); it
    }
    .successMap {
      println("Success $it")
    }
}
Success User(id=1, name=Massimo, email=max@maxcarli.it)

Applicative functors and semigroups

As mentioned, the previous code doesn’t allow you to get all the validation errors, only the first. Open ValidationSemigroup.kt and add the following code:

fun main() {
  val userBuilder = ::User.curry()
  val userApplicative = ResultAp.success(userBuilder)
  val idAp = ResultAp.success(1)
  (userApplicative appl
      idAp appl
      validateName("") appl // HERE
      validateEmail("")) // HERE
    .errorMap {
      println("Error: $it"); it
    }
    .successMap {
      println("Success $it")
    }
}
Error: com.raywenderlich.fp.validation.ValidationException: Invalid email
interface Semigroup<T> {
  operator fun plus(rh: T): T
}
data class ValidationExceptionComposite( // 1
  private val errors: List<ValidationException> // 2
) : Exception(), Semigroup<ValidationExceptionComposite> {

  override fun plus(
    rh: ValidationExceptionComposite
  ): ValidationExceptionComposite =
    ValidationExceptionComposite(this.errors + rh.errors) // 3

  override fun getLocalizedMessage(): String {
    return errors.joinToString { it.localizedMessage } // 4
  }
}
fun <E, T, R> ResultAp<E, T>.apsg(
  fn: ResultAp<E, (T) -> R>
): ResultAp<E, R> where E : Throwable, E : Semigroup<E> = // 1
  when (fn) {
    is Success<(T) -> R> -> successMap(fn.value)
    is Error<E> -> when (this) {
      is Success<T> -> Error(fn.error)
      is Error<E> -> Error(this.error + fn.error) // 2
    }
  }
infix fun <E, T, R> ResultAp<E, (T) -> R>.applsg(
  a: ResultAp<E, T>
) where E : Throwable, E : Semigroup<E> = a.apsg(this)
fun validateNameSg(
  name: String
): ResultAp<ValidationExceptionComposite, String> =
  if (name.length > 4) {
    Success(name)
  } else {
    Error(ValidationExceptionComposite(
      listOf(ValidationException("Invalid name"))
    ))
  }

fun validateEmailSg(
  email: String
): ResultAp<ValidationExceptionComposite, String> =
  if (email.contains("@")) {
    Success(email)
  } else {
    Error(ValidationExceptionComposite(
      listOf(ValidationException("Invalid email"))
    ))
  }
fun main() {
  val userBuilder = ::User.curry()
  val userApplicative = ResultAp.success(userBuilder)
  val idAp = ResultAp.success(1)
  (userApplicative applsg
      idAp applsg
      validateNameSg("") applsg
      validateEmailSg(""))
    .errorMap {
      println(it.localizedMessage); it
    }.successMap {
      println("Success $it")
    }
}
Invalid email, Invalid name
Invalid name
Success User(id=1, name=Massimo, email=max@maxcarli.it)

The Kotlin Result<T> data type

As mentioned earlier, the Kotlin standard library has a Result<T> type that is similar to ResultAp<E, T>, which you implemented earlier.

Result<T> as a functor

To prove that Result<T> is a functor, you should verify the functor laws, and in particular, that:

@InlineOnly
@SinceKotlin("1.3")
public inline fun <R, T> Result<T>.map(
  transform: (value: T) -> R
): Result<R> {
  contract {
    callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
  }
  return when {
    isSuccess -> Result.success(transform(value as T))
    else -> Result(value)
  }
}
public inline fun <R, T> Result<T>.map(): Result<R> {
  return when {
    isSuccess -> Result.success(id(value))
    else -> Result(value)
  }
}
public inline fun <R, T> Result<T>.map(): Result<R> =
  Result.success(id(value))
public inline fun <R, T> Result<T>.map(): Result<R> =
  Result.success(value)
public inline fun <R, T> Result<T>.map(): Result<R> =
  Result(value)

Result<T> as a monad

As mentioned at the end of the previous section, the Kotlin Result<T> data type doesn’t have a flatMap. What happens, then, if you need to compose a function of type (A) -> Result<B> with a function of type (B) -> Result<C>? Well, in Chapter 13, “Understanding Monads”, you learned how to handle this case with a generic data type M<A>. It’s time to do the same for Result<T>.

infix fun <A, B, C> Fun<A, Result<B>>.fish( // 1
  g: Fun<B, Result<C>>
): (A) -> Result<C> =
  { a: A ->
    this(a).bind(g)
  }

infix fun <B, C> Result<B>.bind( // 2
  g: Fun<B, Result<C>>
): Result<C> =
  map(g).flatten()
fun <A> Result<Result<A>>.flatten(): Result<A> = // 1
  if (isSuccess) {
    getOrNull()!! // 2
  } else {
    Result.failure(exceptionOrNull()!!) // 3
  }
fun <A> Result<A>.lift(value: A): Result<A> = // 1
  Result.success(value)

fun <A, B> Result<A>.flatMap(fn: Fun<A, Result<B>>): Result<B> =
  map(::lift fish fn).flatten() // 2

Using Result<T> as a monad

Open ShowSearchService.kt in result and add the following code:

fun fetchTvShowResult(query: String): Result<String> = try {
  Result.success(TvShowFetcher.fetch(query)) // 1
} catch (ioe: IOException) {
  Result.failure(ioe) // 2
}

fun parseTvShowResult(json: String): Result<List<ScoredShow>> =
  try {
    Result.success(TvShowParser.parse(json /* +"sabotage" */)) // 1
  } catch (e: SerializationException) {
    Result.failure(e) // 2
  }
fun fetchAndParseTvShowResult(query: String) =
  fetchTvShowResult(query)  // 1
    .flatMap(::parseTvShowResult) // 2
fun main() {
  fetchAndParseTvShowResult("Big Bang Theory")
    .fold(onFailure = {
      println("Error: $it")
    }, onSuccess = {
      println("Result: $it")
    })
}
Result: [ScoredShow(score=1.2637222, show=Show(id=66, name=The Big Bang Theory, genres=[Comedy], url=https://www.tvmaze.com/shows/66/the-big-bang-theory, image=ShowImage(original=https://static.tvmaze.com/uploads/images/original_untouched/173/433868.jpg, medium=https://static.tvmaze.com/uploads/images/medium_portrait/173/433868.jpg), summary=<p><b>...</p>, language=English))]
Error: java.net.UnknownHostException: api.tvmaze.com
Error: kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 1589: Expected EOF after parsing, but had s instead
JSON input: .....ze.com/episodes/1646220}}}}]sabotage

Meet the RayTV app

In the first part of the chapter, you learned how to handle exceptions in a functional way. You implemented two functions for fetching and parsing some data about TV shows using APIs provided by TVmaze.

Figure 14.4: RayTV app
Cesasa 19.0: GodYM ejs

Figure 14.5: TV show search results
Foriru 71.4: KB qdaf goecbh jimajps

Figure 14.6: TV show details
Ziwiho 39.5: KH lfap viyaixb

fun fetchTvShowResult(query: String): Result<String> =
  try {
    Result.success(TvShowFetcher.fetch(query))
  } catch (ioe: IOException) {
    Result.failure(ioe)
  }

fun parseTvShowResult(json: String): Result<List<ScoredShow>> =
  try {
    Result.success(TvShowParser.parse(json /* +"sabotage" */))
  } catch (e: SerializationException) {
    Result.failure(e)
  }

fun fetchAndParseTvShowResult(query: String) =
  fetchTvShowResult(query)
    .flatMap(::parseTvShowResult)  
@HiltViewModel
class SearchViewModel @Inject constructor() : ViewModel() {

  var searchState = mutableStateOf<SearchState>(NoSearchDone) // 1
    private set

  private var currentJob: Job? = null

  fun findShow(showName: String) {
    currentJob?.cancel()
    currentJob = viewModelScope.launch(Dispatchers.IO) {
      searchState.value = SearchRunning // 1
      fetchAndParseTvShowResult(showName) // 2
        .fold(onFailure = { // 3
          searchState.value = FailureSearchResult(it)
        }, onSuccess = { // 4
          if (!it.isEmpty()) {
            searchState.value = SuccessSearchResult(it)
          } else {
            searchState.value = NoSearchResult // 5
          }
        })
    }
  }
}
// ...
  ErrorAlert(errorMessage = {
    stringResource(R.string.error_message)
  }) {
    result is FailureSearchResult
  }
// ...
Figure 14.7: RayTV error message
Leceyo 22.9: MivCQ uvqig geqpubi

Key points

  • Error handling is a fundamental part of any software application.
  • Many programming languages model errors using exceptions.
  • Exceptions are a classic example of side effects.
  • For exceptions, you can use the same process you used for other impure functions: Make the side effect a part of the result type.
  • You can use Optional<T> and Either<E, T> to model functions throwing exceptions.
  • Applicative functors and semigroups are useful in the case of multiple validations.
  • The Kotlin standard library provides the Result<T> data type.
  • Result<T> is a functor but not a monad.
  • You can make Result<T> a monad by following the same process you used in Chapter 13, “Understanding Monads”.

Where to go from here?

Congratulations! In this chapter, you had the chance to apply all the principles and concepts you learned in the first two parts of the book in a real example. In the next chapter, you’ll learn everything you need to know about state.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.