Robust error-handling for Kotlin and Android

Overview

Belay — Robust Error-Handling for Kotlin and Android

CI Maven Central code size Kind Speech

Code Complete: A Practical Handbook of Software Construction, on error-handling techniques:

Consumer applications tend to favor robustness to correctness. Any result whatsoever is usually better than the software shutting down. The word processor I'm using occasionally displays a fraction of a line of text at the bottom of the screen. If it detects that condition, do I want the word processor to shut down? No. I know that the next time I hit Page Up or Page Down, the screen will refresh and the display will be back to normal.

Belay is a Kotlin error-handling library which favors robustness. It serves two purposes:

  • Detect errors early during development using assertions.
  • Gracefully recover from errors when they occur in production.

Installation

In your project's build.gradle:

dependencies {
    implementation("dev.specto:belay:0.3.0")
}

Declare a top level variable to run expectations:

val expect = Expect()

It can be named anything, but for the purposes of this documentation we'll assume it's named "expect".

Next, set the top level expectation handler as early as possible during your program's initialization:

fun main() {
    expect.onGlobalFail = object : GlobalExpectationHandler() {
        override fun handleFail(exception: ExpectationException) {
            if (DEBUG) throw exception
            else log(exception.stackTraceToString())
        }
    }
    
    //
}

The global handler will be invoked when any expectations fail. Of course it's possible, and often desirable, to handle expectations individually or by subgroups, but the global handler is the ideal place to throw exceptions during development—effectively turning expectations into assertions—so that failures can be noticed and addressed immediately. In production it can be used to log all errors, regardless of how they are ultimately handled, so that they can be fixed at a later date.

⚠️ Do not call expect from the global handler, it could cause an infinite loop if that expectation fails.

Writing Expectations

The expect variable provides a host of utilities to write expectations and specify how they should be handled. They fall in 3 main categories.

Global Expectations

Global expectations only invoke the global handler when they fail.

expect(condition, optionalErrorMessage) // shorthand for expect.isTrue(…)
expect.fail("error message", optionalCause)
expect.isTrue(condition, optionalErrorMessage)
expect.isFalse(condition, optionalErrorMessage)
expect.isNotNull(value, optionalErrorMessage)
expect.isNull(value, optionalErrorMessage)
expect.isType<Type>(value, optionalErrorMessage)

Unless the global handler interrupts the program, it will proceed even when these expectations fail. Therefore, they are meant to be used when the program can proceed even when the expectations fail. For example:

fun cleanup() {
    expect.isNotNull(configuration)
    configuration = null
}

Locally-Handled Expectations

Locally-handled expectations invoke the global handler when they fail, and then a locally-defined error-handling function.

expect(condition, optionalErrorMessage) {
    // Handle the expectation failing.
    // Does not need to return or throw an exception.
}

expect.isTrue(condition, optionalErrorMessage) {
    // Handle the expectation failing.
    // Must return or throw an exception which enables smart casting.
    return
}
expect.isFalse(condition, optionalErrorMessage) { … }
val nonNullValue = expect.isNotNull(value, optionalErrorMessage) { … }
expect.isNull(value, optionalErrorMessage) { … }
val valueCastToType = expect.isType<Type>(value, optionalErrorMessage) { … }

A custom error message can be provided to all these functions.

This is great for one-off error-handling:

fun startTime(): Long {
    //
    
    expect(startTime >= 0, "startTime was negative") {
        startTime = 0
    }
    
    return startTime
}

fun animate() {
    expect.isNotNull(animator) { return }
    
    //
    animator.animate(…)
}

Expectation Blocks

Often the same error-handling strategy can be used across individual functions or blocks of code. Expectation blocks make this easy.

expect(blockHandler) {
    fail("error message", optionalCause)
    isTrue(condition, optionalErrorMessage)
    isFalse(condition, optionalErrorMessage)
    isNotNull(value, optionalErrorMessage)
    isNull(value, optionalErrorMessage)
    isType<Type>(value, optionalErrorMessage)
}

A custom error message can be provided to all these functions.

Several block handlers are offered out of the box.

Continue does nothing when an expectation fails (besides invoking the global handler):

fun stop() = expect(onFail = Continue) {
    isTrue(isActive, "time machine was not active when stop was called")
    isNotNull(configuration)
    isNotNull(controller)
    
    isActive = false
    configuration = null
    controller = null
    //
}

Return immediately returns a default value when an expectation fails:

fun startTime(): Long = expect(Return(0)) {
    //
}

ReturnLast returns the last value returned or a default if no value has been returned yet:

fun pixelColor(x: Int, y: Int): Color = expect(ReturnLast(Color.BLACK)) {
    //
}

Throw throws an exception when an expectation fails:

fun startRadiationTreatment() = expect(Throw) {
    //
}

All the provided block handlers allow an arbitrary function to be executed when an expectation fails:

expect(Return { disableController() }) {
    //
}

Block handlers which interrupt the program, like Return, ReturnLast and Throw, can also treat exceptions as failed expectations:

fun startTime(): Long = expect(Return(0), catchExceptions = true) {
    // All exceptions thrown by this function will be automatically caught
    // and handled by the expectation handler as a failed expectation.
}

It's also possible, and easy, to write your own expectation handler.

Writing Expectation Handlers

Writing custom expectation handlers is particularly useful when the same custom logic needs to be reused across a program. There are two types of expectation handlers: those that may interrupt the program when an expectation fails, and those that definitely do.

Handlers who may interrupt the program when an expectation fails, like Continue, must extend ContinueExpectationHandler. Handlers that definitely interrupt the program, for example by returning early or throwing an exception, like Return, ReturnLast or Throw, should extend ExitExpectationHandler. This distinction serves to enable smart casting for ExitExpectationHandler expectations.

You've actually already implemented a handler which uses the same interface as ContinueExpectationHandler, the global handler. The ExitExpectationHandler interface is very similar, here's an example implementation:

class DisableControllerAndReturn<T>(
    returnValue: T,
    also: ((exception: ExpectationException) -> Unit)? = null
) : ExitExpectationHandler<T>() {

    private val controller: Controller by dependencyGraph

    override fun handleFail(exception: ExpectationException): Nothing {
        controller.disable(exception.message)
        also?.invoke(exception)
        returnFromBlock(returnValue)
    }
}

Contributing

We love contributions! Check out our contributing guidelines and be sure to follow our code of conduct.

You might also like...
Clean Android multi-module offline-first scalable app in 2022. Including Jetpack Compose, MVI, Kotlin coroutines/Flow, Kotlin serialization, Hilt and Room.

Android Kotlin starter project - 2022 edition Android starter project, described precisely in this article. Purpose To show good practices using Kotli

Integration Testing Kotlin Multiplatform Kata for Kotlin Developers. The main goal is to practice integration testing using Ktor and Ktor Client Mock
Integration Testing Kotlin Multiplatform Kata for Kotlin Developers. The main goal is to practice integration testing using Ktor and Ktor Client Mock

This kata is a Kotlin multiplatform version of the kata KataTODOApiClientKotlin of Karumi. We are here to practice integration testing using HTTP stub

Spring-kotlin - Learning API Rest with Kotlin, Spring and PostgreSQL

Kotlin, Spring, PostgreSQL and Liquibase Database Migrations Learning Kotlin for

FlowExt is a Kotlin Multiplatform library, that provides many operators and extensions to Kotlin Coroutines Flow

FlowExt | Kotlinx Coroutines Flow Extensions | Kotlinx Coroutines Flow Extensions. Extensions to the Kotlin Flow library | kotlin-flow-extensions | Coroutines Flow Extensions | Kotlin Flow extensions | kotlin flow extensions | Flow extensions

This Kotlin Multiplatform library is for accessing the TMDB API to get movie and TV show content. Using for Android, iOS, and JS projects.

Website | Forum | Documentation | TMDb 3 API Get movie and TV show content from TMDb in a fast and simple way. TMDb API This library gives access to T

High performance and fully asynchronous pulsar client with Kotlin and Vert.x

pulsarkt High performance pulsar client with Kotlin and Vert.x Features Basic Producer/Consumer API Partitioned topics Batching Chunking Compression T

Microservices-demo - Microservices demo project using Spring, Kotlin, RabbitMQ, PostgreSQL and Gradle and deployed to Azure Kubernetes An investigation and comparison between Kotlin and Java on an engineering level
An investigation and comparison between Kotlin and Java on an engineering level

An investigation and comparison between Kotlin and Java on an engineering level. Since beauty is in the eye of the beholder, this repository is not meant to evaluate Java or Kotlin on an aesthetic level.

An application with the use of Kotlin can change the color of the text, and the background with the press of a button and switch.
An application with the use of Kotlin can change the color of the text, and the background with the press of a button and switch.

An application with the use of Kotlin can change the color of the text, and the background with the press of a button and switch.

Comments
  • Swift-like guard statement

    Swift-like guard statement

    A few people have been comparing parts of Belay to the Swift guard statement so I looked to see if there was any inspiration to be found there.

    Here in its own basic form:

    guard condition else {
        // statements
        // exit
    }
    

    It is very similar to:

    expect.isTrue(condition) {
        // statements
        // exit
    }
    

    Belay also provides a variant that is very useful when a false condition can be handled without exiting from the parent:

    expect(condition) {
        // statements
        // does not need to exit
    }
    

    And of course if one doesn't need to handle expectations globally it's just as easy to write:

    if (!condition) {
        // …
    }
    

    The Swift guard also supports an "optional binding declaration":

    guard let constantName = someOptional else {
        // …
    }
    

    Which is very similar to:

    val constantName = expect.isNotNull(someOptional) {
        // …
    }
    

    One major difference: Swift support multiple bindings, Belay does not in this case.

    And another: the variables assigned a value from an optional binding can be used as part of the condition:

    guard let constantName = someOptional, condition(constantName) else {
        // …
    }
    
    enhancement 
    opened by ngsilverman 0
  • Guard against infinite expectation loops, especially in global handlers

    Guard against infinite expectation loops, especially in global handlers

    Using expectations instead of handlers, especially the global one, could cause infinite loops when they fail.

    expect.onGlobalFail = object : GlobalExpectationHandler() {
        override fun handleFail(exception: ExpectationException) {
            expect.fail("this will trigger an infinite loop as soon as an expectation fails")
        }
    }
    

    This is easy to avoid when the global handler logic is simple, which I expect to be the majority of cases, but it could be done inadvertently for more complex handlers who make various function calls, for example to disable parts of the application.

    Since the purpose of the library is in part to be able to recover critical failures, it would be unfortunate to cause one. Ideally we would detect loops that are likely infinite and end them before a stack overflow. The approach could perhaps consist of detecting if multiple calls to handleFail are occurring without any of the calls returning. By inspecting the ExpectationException one could make an educated guess as to whether the same expectation is failing in a loop.

    enhancement 
    opened by ngsilverman 4
  • Update Kotlin to 1.4.10+ once Gradle updates, enable explicit API mode

    Update Kotlin to 1.4.10+ once Gradle updates, enable explicit API mode

    I've held off on updating Kotlin because Gradle 6.7 still uses Kotlin 1.3.72 and so using a library like Belay in a Gradle plugin can cause conflicts.

    opened by ngsilverman 0
  • Add take function (similar to takeIf)

    Add take function (similar to takeIf)

    Rather than:

    fun countBananas(): Int {
        // Count the bananas.
    
        expect(bananaCount >= 0, "Invalid count: $bananaCount.") {
            bananaCount = 0
        }
        return bananaCount
    }
    

    Something like:

    fun countBananas(): Int {
        // Count the bananas.
    
        return expect.take(bananaCount, "Invalid count: $bananaCount.") { it >= 0 } ?: 0
    }
    
    enhancement 
    opened by ngsilverman 0
Releases(v0.3.0)
  • v0.3.0(Nov 20, 2020)

    Added

    • Global expectations.
    • Locally-handled expectations.
    • Expectation blocks.
    • Built-in expectation handlers:
      • Continue
      • Return
      • ReturnLast
      • Throw
    • Custom expectation handlers.
    Source code(tar.gz)
    Source code(zip)
Owner
Specto Inc.
Mobile App Performance Management in the Cloud
Specto Inc.
🧶 Library to handling files for persistent storage with Google Cloud Storage and Amazon S3-compatible server, made in Kotlin

?? Remi Library to handling files for persistent storage with Google Cloud Storage and Amazon S3-compatible server, made in Kotlin! Why is this built?

Noelware 8 Dec 17, 2022
🌨️ Simple, intuitive, and opinionated command handling library for Kord

??️ Snow Simple, intuitive, and opinionated command handling library for Kord Why? Since I maintain two Discord bots, both in Kotlin, Nino and Noel (p

Noel ʕ •ᴥ•ʔ 1 Jan 16, 2022
Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP)

Mockative Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP). Installation Mockative uses KSP to generate

Mockative 121 Dec 26, 2022
🎲 Kotlin Symbol Processor to auto-generate extensive sealed classes and interfaces for Android and Kotlin.

SealedX ?? Kotlin Symbol Processor to auto-generate extensive sealed classes and interfaces for Android and Kotlin. Why SealedX? SealedX generates ext

Jaewoong Eum 236 Nov 30, 2022
Kotlin microservices with REST, and gRPC using BFF pattern. This repository contains backend services. Everything is dockerized and ready to "Go" actually "Kotlin" :-)

Microservices Kotlin gRPC Deployed in EC2, Check it out! This repo contains microservices written in Kotlin with BFF pattern for performing CRUD opera

Oguzhan 18 Apr 21, 2022
Repo: Programming problems with solutions in Kotlin to help avid Kotlin learners to get a strong hold on Kotlin programming.

Kotlin_practice_problems Repo: Programming problems with solutions in Kotlin to help avid Kotlin learners to get a strong hold on Kotlin programming.

Aman 0 Oct 14, 2021
Kotlin-oop - Repositório criado para ser utilizado pelo projeto de Kotlin OOP desenvolvido em Kotlin nas aulas feitas através da plataforma Alura.

Projeto React OOP Repositório criado para ser utilizado pelo projeto de Kotlin OOP desenvolvido em Kotlin nas aulas feitas através da plataforma Alura

Marcos Felipe 1 Jan 5, 2022
Kotlin-koans - Kotlin Koans are a series of exercises to get you familiar with the Kotlin Syntax

kotlin-koans-edu Kotlin Koans are a series of exercises to get you familiar with

null 1 Jan 11, 2022
Jetpack Compose for Desktop and Web, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.

Jetpack Compose for Desktop and Web, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.

JetBrains 10k Jan 7, 2023
Modular Android architecture which showcase Kotlin, MVVM, Navigation, Hilt, Coroutines, Jetpack compose, Retrofit, Unit test and Kotlin Gradle DSL.

SampleCompose Modular Android architecture which showcase Kotlin, MVVM, Navigation, Hilt, Coroutines, Jetpack compose, Retrofit, Unit test and Kotlin

Mohammadali Rezaei 7 Nov 28, 2022