Every program should grapple with failure. A happy path is only a part of the story. There are many ways to handle errors. The essential toolkit showcases throwing and catching exceptions, passing results, leveraging optionals, aborting, and asserting.

Exceptions

If there’s an unusual situation that you cannot or don’t want to react to, you can throw an exception. It’s a sort of failure that will stop your current flow of execution and if you don’t handle it eventually, it will terminate your program with an error. This is the most common way to deal with errors.

Throwing exceptions

You can throw an exception anywhere and you are not required to declare or handle it. It will bubble up the call stack.

Quick and dirty. Ideally, you would use a more specific subtype of exception to provide more details about what happened.

throw Exception()
throw Exception("Something bad happened")

Root hierarchy. Every exception is Throwable and will be a child of one of these:

val baseExceptions: Iterable<Throwable> = listOf(
  Exception("Errors that make sense to handle"),
  RuntimeException(
    "A programming error that can be prevented by refactoring the code"
  ),
  Error("Errors you can't do much about")
)

for (e in baseExceptions) throw e

Non-recoverable errors:

val fatalErrors: Iterable<Error> = listOf(
  OutOfMemoryError(),
  StackOverflowError()
)

Exceptions that can be handled:

val anyExceptionToHandle: Iterable<Exception> = listOf(
  RuntimeException(),
  java.io.IOException(),
  java.util.zip.DataFormatException()
)

Exceptions that can be prevented by writing better code, usually result of validation:

val runtimeIssues: Iterable<RuntimeException> = listOf(
  IllegalArgumentException(),
  IllegalStateException(),
  ArithmeticException()
)

Custom exceptions

class CorruptRemoteDataError: Error(
  "Bad external data that renders the app totally unusable"
) {}

class ImportFailedException: Exception(
  "An external cause that we can handle gracefully"
) {}

class OldCacheFormatException: RuntimeException(
  "Edge case that can be prevented by more checks, " +
  "or it can serve as a trigger to handle a rare special case"
) {}

Preconditions

There are handy functions that can validate input parameters and invariants quickly: require, check, error, with ...NotNull flavors.

var state: Map<String, Boolean> = mapOf("initialized" to true)

fun doSomething(input: Collection<String>): String {
  require(input.isNotEmpty()) // throw IllegalArgumentException()

  val initialized = 
    checkNotNull(state["initialized"]) // throw IllegalStateException()
  check(initialized) // throw IllegalStateException()

  return input.joinToString(", ") {
    if (it == "BANG!")
      error("Invalid instruction!") // throw IllegalStateException()
    else
      it
  }
}

[--]