Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Beyond Exceptions: Building Resilient Android A...

Beyond Exceptions: Building Resilient Android Apps with Safety-Critical Principles

When your Android app crashes, users uninstall. This session explores how to handle failures before they become crashes, focusing on Android's unique reliability challenges.

We'll establish the critical distinction between domain errors (expected business logic failures) and system failures (unrecoverable hardware/OS issues), implementing each with appropriate strategies. You'll learn to build a custom Result monad that provides compile-time safety beyond Kotlin's built-in limitations, and we'll see how NASA's mission-critical safety rules can be applied in the Android world. You'll also master native crash debugging techniques for those unavoidable system failures.

From handling hardware state corruption to graceful degradation under memory pressure, you'll walk away with battle-tested patterns for Android's trickiest reliability scenarios: complex state management, native code integration, and building apps that degrade gracefully rather than crash catastrophically.

Target audience: Individual contributors and engineering managers looking to improve app stability and reduce crash rates through principled error handling.

Avatar for Bogusz Pawłowski

Bogusz Pawłowski

September 22, 2025
Tweet

Other Decks in Programming

Transcript

  1. Bogusz Pawłowski Sta ff Engineer @SpotOn • Complex restaurant POS

    system • O ff line- f irst approach • Most of the logic on the client side
  2. Bogusz Pawłowski Sta ff Engineer @SpotOn • Complex restaurant POS

    system • O ff line- f irst approach • Most of the logic on the client side • Multiple applications with local and IP communication
  3. Bogusz Pawłowski • Complex restaurant POS system • O ff

    line- f irst approach • Most of the logic on the client side • Multiple applications with local and IP communication • Huge domain Sta ff Engineer @SpotOn
  4. but

  5. Java 17 SE documentation “The class Exception and its subclasses

    are a form of Throwable that indicates conditions that a reasonable application might want to catch.” “An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.”
  6. Domain Errors Program Logic Errors Are anticipated? Can be handled

    gracefully? What Kotlin features should we use?
  7. Domain Errors Program Logic Errors Are anticipated? Yes Can be

    handled gracefully? What Kotlin features should we use?
  8. Domain Errors Program Logic Errors Are anticipated? Yes No Can

    be handled gracefully? What Kotlin features should we use?
  9. Domain Errors Program Logic Errors Are anticipated? Yes No Can

    be handled gracefully? Always What Kotlin features should we use?
  10. Domain Errors Program Logic Errors Are anticipated? Yes No Can

    be handled gracefully? Always Usually no What Kotlin features should we use?
  11. Domain Errors Program Logic Errors Are anticipated? Yes No Can

    be handled gracefully? Always Usually no What Kotlin features should we use? ? ?
  12. val invalidInput = "123132123a" val phoneNumber: PhoneNumber = invalidInput.toPhoneNumber() println("Acquired

    phone number: $phoneNumber") 💥 fun String.toPhoneNumber(): PhoneNumber
  13. val invalidInput = "123132123a" try { val phoneNumber: PhoneNumber =

    invalidInput.toPhoneNumber() println("Acquired phone number: $phoneNumber") } catch (e: IllegalArgumentException) { println("Failed to acquire phone number: ${e.message}") } fun String.toPhoneNumber(): PhoneNumber
  14. val invalidInput = "123132123a" // Requires knowledge of implementation details

    to handle the exception. try { val phoneNumber: PhoneNumber = invalidInput.toPhoneNumber() println("Acquired phone number: $phoneNumber") } catch (e: IllegalArgumentException) { println("Failed to acquire phone number: ${e.message}") } fun String.toPhoneNumber(): PhoneNumber
  15. val invalidInput = "123132123a" // Requires knowledge of implementation details

    to handle the exception. try { val phoneNumber: PhoneNumber = invalidInput.toPhoneNumber() println("Acquired phone number: $phoneNumber”) // We collect whole stack trace, which is completely pointless } catch (e: IllegalArgumentException) { println("Failed to acquire phone number: ${e.message}") } fun String.toPhoneNumber(): PhoneNumber
  16. val invalidInput = "123132123a" val phoneNumber: PhoneNumber? = invalidInput.toPhoneNumberOrNull() if

    (phoneNumber == null) { println("Failed to acquire phone number: Invalid input provided") } else { println("Acquired phone number: ${phoneNumber.raw}") } fun String.toPhoneNumberOrNull(): PhoneNumber?
  17. val invalidInput = "123132123a" val phoneNumber: PhoneNumber? = invalidInput.toPhoneNumberOrNull() if

    (phoneNumber == null) { // This requires knowledge of implementation details println("Failed to acquire phone number: Invalid input provided") } else { println("Acquired phone number: ${phoneNumber.raw}") } fun String.toPhoneNumberOrNull(): PhoneNumber?
  18. val invalidInput = "123132123a" val phoneNumber: PhoneNumber? = invalidInput.toPhoneNumberOrNull() //

    We are limited to only one possible error case if (phoneNumber == null) { // This requires knowledge of implementation details println("Failed to acquire phone number: Invalid input provided") } else { println("Acquired phone number: ${phoneNumber.raw}") } fun String.toPhoneNumberOrNull(): PhoneNumber?
  19. fun String.toPhoneNumber(): Result<PhoneNumber>f{f return if (isBlank())f{f Result.failure(IllegalArgumentException("Number cannot be blank"))

    } else if (!matches(regex =fPatterns.PHONE.toRegex()))f{f Result.failure(IllegalArgumentException("Invalid phonefnumber")) } elsef{f Result.success(PhoneNumber(this)) }f }ff
  20. val invalidInput = "123132123a" val phoneNumberResult: Result<PhoneNumber> = invalidInput.toPhoneNumber() phoneNumberResult.onSuccess

    { phoneNumber -> println("Acquired phone number: ${phoneNumber.raw}") } fun String.toPhoneNumber(): Result<PhoneNumber>
  21. val invalidInput = "123132123a" val phoneNumberResult: Result<PhoneNumber> = invalidInput.toPhoneNumber() phoneNumberResult.onSuccess

    { phoneNumber -> println("Acquired phone number: ${phoneNumber.raw}") }.onFailure { exception -> when (exception) { is IllegalArgumentException -> { TODO() } else -> { println("An unexpected error occurred") } } } fun String.toPhoneNumber(): Result<PhoneNumber>
  22. phoneNumberResult.onFailure { exception -> when (exception) { is IllegalArgumentException ->f{

    TODO() } else -> { println("An unexpected error occurred") } } } fun String.toPhoneNumber(): Result<PhoneNumber>
  23. phoneNumberResult.onFailure { exception -> when (exception) { is IllegalArgumentException ->f{

    when (exception.message) { "Number cannot be blank" -> println("Number cannot be blank") "Invalid phone number" -> println("$invalidInput is invalid") else -> println("An unexpected error occurred”) } } else -> { println("An unexpected error occurred") } } } fun String.toPhoneNumber(): Result<PhoneNumber>
  24. phoneNumberResult.onFailure { exception -> when (exception) { // Pointless exception

    creation is IllegalArgumentException -> { when (exception.message) { "Number cannot be blank" -> println("Number cannot be blank") "Invalid phone number" -> println("$invalidInput is invalid") else -> println("An unexpected error occurred”) } } else -> { println("An unexpected error occurred") } } } fun String.toPhoneNumber(): Result<PhoneNumber>
  25. phoneNumberResult.onFailure { exception -> when (exception) { // Pointless exception

    creation is IllegalArgumentException -> { when (exception.message) { "Number cannot be blank" -> println("Number cannot be blank") "Invalid phone number" -> println("$invalidInput is invalid") else -> println("An unexpected error occurred”) } } // This will never be exhaustive, requires implementation knowledge else -> { println("An unexpected error occurred") } } } fun String.toPhoneNumber(): Result<PhoneNumber>
  26. phoneNumberResult.onFailure { exception -> when (exception) { // Pointless exception

    creation is IllegalArgumentException -> { when (exception.message) { "Number cannot be blank" -> println("Number cannot be blank") "Invalid phone number" -> println("$invalidInput is invalid") else -> println("An unexpected error occurred”) } } // This will never be exhaustive, requires implementation knowledge // When the implementation changes, we won’t know about it. else -> { println("An unexpected error occurred") } } } fun String.toPhoneNumber(): Result<PhoneNumber>
  27. https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md#use-cases “The Result class is designed to capture generic failures

    of Kotlin functions for their latter processing and should be used in general-purpose API like futures (…). The Result class is not designed to represent domain-speci f ic error conditions.”
  28. https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md#use-cases “The Result class is designed to capture generic failures

    of Kotlin functions for their latter processing and should be used in general-purpose API like futures, etc, that deal with invocation of Kotlin code blocks and must be able to represent both a successful and a failed result of execution. The Result class is not designed to represent domain-speci f ic error conditions.” “In general, if some API requires its callers to handle failures locally (…), then it should use nullable types, when these failures do not carry additional business meaning, or domain-speci f ic data types to represent its successful results and failures (…)”
  29. sealed interface Result<out T, out E> { data class Success<T>(val

    value: T) : Result<T, Nothing> data class Failure<E>(val error: E) : Result<Nothing, E> }
  30. fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError> { return when { this.isBlank() ->

    Result.Failure(PhoneValidationError.BlankNumber) !this.matches(regex = Patterns.PHONE.toRegex()) -> Result.Failure( PhoneValidationError.InvalidFormat ) else -> Result.Success(PhoneNumber(this)) } }
  31. val invalidInput = "123132123a" val phoneNumberResult: Result<PhoneNumber, PhoneValidationError> = invalidInput.toPhoneNumber()

    when (phoneNumberResult) { is Result.Success -> TODO() is Result.Failure -> TODO() } fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError>
  32. val invalidInput = "123132123a" val phoneNumberResult: Result<PhoneNumber, PhoneValidationError> = invalidInput.toPhoneNumber()

    when (phoneNumberResult) { is Result.Success -> println("Acquired phone number: $phoneNumberResult") is Result.Failure -> { when (phoneNumberResult.error) { BlankNumber -> TODO() InvalidFormat -> TODO() } } } fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError>
  33. val invalidInput = "123132123a" val phoneNumberResult: Result<PhoneNumber, PhoneValidationError> = invalidInput.toPhoneNumber()

    when (phoneNumberResult) { is Result.Success -> println("Acquired phone number: $phoneNumberResult") is Result.Failure -> { when (phoneNumberResult.error) { BlankNumber -> TODO() InvalidFormat -> TODO() // No ‘else’!! } } } fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError>
  34. val invalidInput = "123132123a" val phoneNumberResult: Result<PhoneNumber, PhoneValidationError> = invalidInput.toPhoneNumber()

    when (phoneNumberResult) { is Result.Success -> println("Acquired phone number: $phoneNumberResult") is Result.Failure -> { when (phoneNumberResult.error) { BlankNumber -> println("Invalid input: Phone number cannot be blank") InvalidFormat -> println("Invalid format for number $invalidInput") } } } fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError>
  35. sealed interface CommonError { interface BlankInput : CommonError } fun

    handleCommonErrors(error: CommonError) { when (error) { is CommonError.BlankInput -> println("Error: Input cannot be blank") } }
  36. sealed interface CommonError { interface BlankInput : CommonError } fun

    handleCommonErrors(error: CommonError) { when (error) { is CommonError.BlankInput -> println("Error: Input cannot be blank") } } sealed interface PhoneValidationError { data object BlankNumber : PhoneValidationError data object InvalidFormat : PhoneValidationError }
  37. sealed interface CommonError { interface BlankInput : CommonError } fun

    handleCommonErrors(error: CommonError) { when (error) { is CommonError.BlankInput -> println("Error: Input cannot be blank") } } sealed interface PhoneValidationError { data object BlankNumber : PhoneValidationError, CommonError.BlankInput data object InvalidFormat : PhoneValidationError }
  38. when (phoneNumberResult) { is Result.Success -> println("Acquired phone number: $phoneNumberResult")

    is Result.Failure -> { when (phoneNumberResult.error) { BlankNumber -> println("Invalid input: Phone number cannot be blank") InvalidFormat -> println("Invalid format for number $invalidInput") } } } fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError>
  39. when (phoneNumberResult) { is Result.Success -> println("Acquired phone number: $phoneNumberResult")

    is Result.Failure -> { when (phoneNumberResult.error) { is BlankNumber -> handleCommonError(phoneNumberResult.error) InvalidFormat -> println("Invalid format for number $invalidInput") } } } fun String.toPhoneNumber(): Result<PhoneNumber, PhoneValidationError>
  40. type PhoneNumber struct { Raw string } func ToPhoneNumber(s string)

    (PhoneNumber, error) { if strings.TrimSpace(s) == "" { return nil, errors.New("number cannot be blank") } return PhoneNumber{Raw: s}, nil }
  41. type PhoneNumber struct { Raw string } func ToPhoneNumber(s string)

    (PhoneNumber, error) { if strings.TrimSpace(s) == "" { return nil, errors.New("number cannot be blank") } return PhoneNumber{Raw: s}, nil } func main() { invalidInput := " " phoneNumber, err := ToPhoneNumber(invalidInput) if err != nil { fmt.Printf("Failed to acquire phone number: %v\n", err) return } fmt.Printf("Acquired phone number: %+v\n", phoneNumber) }
  42. error class BlankInputError error class InvalidFormatError fun String.toPhoneNumber(): PhoneNumber |

    BlankInputError | InvalidFormatError { return when { this.isBlank() -> BlankInputError !this.matches(regex = Patterns.PHONE.toRegex()) -> InvalidFormatError else -> PhoneNumber(this) } }
  43. val invalidInput = "123132123a" val phoneNumberResult: PhoneNumber | BlankInputError |

    InvalidFormatError = invalidInput.toPhoneNumber() when (phoneNumberResult) { is PhoneNumber -> println("Acquired phone number: $phoneNumberResult") is BlankInputError -> handleCommonErrors(BlankInputError) is InvalidPhoneNumberFormatError -> println("Invalid format") } fun String.toPhoneNumber(): PhoneNumber | BlankInputError | InvalidFormatError
  44. val invalidInput = "123132123a" val phoneNumberResult: PhoneNumber | BlankInputError |

    InvalidFormatError = invalidInput.toPhoneNumber() // Safe call (?.) and double bang (!!.) will be available when (phoneNumberResult) { is PhoneNumber -> println("Acquired phone number: $phoneNumberResult") is BlankInputError -> handleCommonErrors(BlankInputError) is InvalidPhoneNumberFormatError -> println("Invalid format") } fun String.toPhoneNumber(): PhoneNumber | BlankInputError | InvalidFormatError
  45. The Power of Ten – Rules for Developing Safety Critical

    Code “Rule: The assertion density of the code should average to a minimum of 2 assertions per function. Assertions are used to check for anomalous conditions that should never happen in real-life execution ”
  46. We can leverage exceptions to spot program logic failures faster

    class Cart { var order: Order? = null fun pay(amount: Double) { require(amount > 0) { "Amount must be greater than zero" } check(order != null) { "Order must be set before payment" } } }
  47. 1. Di ff erentiate between di ff erent error types

    2. Handle your domain errors same way you handle success 1. Use nulls to handle simple error cases 2. Use sealed hierarchies to handle more complex errors 3. When interacting with 3rd party software be sure to guard edges of your system
  48. 1. Di ff erentiate between di ff erent error types

    2. Handle your domain errors same way you handle success 1. Use nulls to handle simple error cases 2. Use sealed hierarchies to handle more complex errors 3. When interacting with 3rd party software be sure to guard edges of your system 3. Use exceptions to spot and f ix program logic failures ASAP 1. Do not be afraid to crash the program, especially in debug 2. Add tools to enable developers to easily report a bug