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

Designing for Errors in a Kotlin-first SDK

Designing for Errors in a Kotlin-first SDK

Error propagation is an important consideration for building feature-rich API architecture in Kotlin. Whereas apps may have the flexibility to add external dependencies and laser-focus on specific use-cases, designing an API requires minimal dependencies and wide applicability. This lightning talk is a crash course in Kotlin error propagation strategies and a case study in our experience designing errors when migrating our SDK from Java to Kotlin. We will go over the various techniques that Kotlin natively provides for us, including throwing exceptions, returning null, sealed classes, and Result, and how we decided which technique to use for our functions.

Avatar for Hudson Miears

Hudson Miears

October 26, 2023

Other Decks in Technology

Transcript

  1. Designing for Errors in a Kotlin-first SDK When to throw,

    nullify, or return a Result Hudson Miears, ArcGIS Maps SDK for Kotlin Team @ Esri
  2. Motivation and Outline • When we were designing our Kotlin

    SDK, we came up against a problem: to throw or not to throw? • It’s inevitable that errors will occur, so we need to have a clear strategy on how the user receives those errors. • When errors happen, we want it to be simple to understand how to deal with them. • In this talk we’ll cover the methods of error propagation Kotlin provides and how we choose which method to use in which situation
  3. Error Propagation? • Part of how you communicate to other

    developers (your users) • There are several methods of error propagation in Kotlin • There is not a lot of guidance • Synchronous and Asynchronous functions behave slightly differently • Methods of propagating errors: • Throwing exceptions • Sealed class hierarchies • Result<T> • Nullable types
  4. ArcGIS Maps SDK for Kotlin • A fully-featured, massive SDK

    with many long-term customers • Follow-on from an existing Java-based Android SDK • Can’t add too many external dependencies • Need to cover a lot of user stories • Sealed class hierarchies are a lot of effort and deviate from other SDKs we offer
  5. Types of Errors • We identified three types of errors

    that we need to propagate • Logic Errors • Domain-specific Errors • Lenient Errors
  6. Logic Errors • The user can predict the error when

    writing the code, and it’s a “clerical error” • These types of errors shouldn’t exist in the code at all, and it should not be possible to recover from them • Example: Argument out of range • Solution: Throw an exception to crash the user’s app • Note: We can also use require() and check() • It’s important to document this behavior
  7. Domain-Specific Errors • If an error occurs, it’s recoverable: the

    user needs to be able to write code to handle any potential errors • In order to write that code, they need to know why the error happened. Usually that means it’s specific to the SDK’s business logic • Example: Geodatabase.beginTransaction() can fail if a transaction is already active on the Geodatabase • We want to propagate information back to the user about the error without crashing their app • Solution: return either the expected value or an error
  8. Domain-Specific Errors • One option: Sealed class hierarchies • Make

    a GeodatabaseTransactionResult sealed class, with a GeodatabaseTransactionResult.Success and a GeodatabaseTransactionResult.ExistingTransactionFailure • The user can write a nice exhaustive when statement • There are lots of talks about how great this is! • However, creating these can be time-consuming and would make us deviate from patterns that we established on other platforms and in our Java SDK
  9. Domain-Specific Errors • Another option. Result<T> • Lets the user

    handle both a success and failure case • Provides all the useful information you expect from Exceptions (stack trace, messages) • No extra work for our use case because we already had exceptions being thrown lower down in the stack • Arrow provides a similar solution in the form of Either
  10. Lenient Errors • They need to handle the failure case

    but the way they handle it won’t depend on why the error occurred • We might not even know why the error occurred (eg. A badly formatted String was passed to the function) • Example: CoordinateFormatter.fromLatitudeOrLongitude(coordinates: String, spatialReference: SpatialReference?) : Point • Solution: return a nullable type • If the function would normally return Unit, return a Boolean instead • For extreme clarity, we add the “orNull” or “try” to the names of these functions • CoordinateFormatter.fromLatitudeLongitudeOrNull(coordinates: String, spatialReference: SpatialReference?): Point?
  11. Suspend Functions • Generally they act the same as synchronous

    functions (yay Kotlin!) • Some caveats: • async will not throw until the user calls await • The user might have a CoroutineExceptionHandler set • Using runCatching can cause issues because CancellationException is how cancellation is propagated in coroutines • In our SDK, we wanted to match the Future pattern that we had used for Java, so we always return a Result from suspend functions
  12. Miscellaneous • We wanted to coordinate with our Swift SDK

    team, but error handling is quite different for Swift! • There’s not a lot of documentation out there on best practices. We wished there was more • Here are some resources we used to learn more: • Roman Elizarov’s “Kotlin and Exceptions” • Kotlin KEEP about Result<T>
  13. TL;DR • If the error is predictable and should never

    occur (ie. It’s caused by a clerical error), then throw an exception • If the error is unpredictable, the programmer needs to write code to handle it, and the programmer needs to know why the error occurred, return a member of a sealed class hierarchy or a Result<T> • If the error is unpredictable, the programmer needs to write codet o handle it, and the programmer doesn’t need to know why it failed (or we don’t even know why ourselves), then the function should return a nullable type or a Boolean