DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Metal and the Simulated Annealing Algorithm
  • Reactive Kafka With Spring Boot
  • Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
  • How To Create a Homescreen Widget in Android

Trending

  • Enforcing Architecture With ArchUnit in Java
  • Chat With Your Knowledge Base: A Hands-On Java and LangChain4j Guide
  • MCP Servers: The Technical Debt That Is Coming
  • GitHub Copilot's New AI Coding Agent Saves Developers Time – And Requires Their Oversight
  1. DZone
  2. Coding
  3. Languages
  4. Kotlin: Functional Exception Handling With Try

Kotlin: Functional Exception Handling With Try

Check out how you can introduce functional exception handling in Kotlin code with some help from the Try type as well as how to use lambdas for chaining.

By 
Biju Kunjummen user avatar
Biju Kunjummen
·
Dec. 28, 17 · Tutorial
Likes (10)
Comment
Save
Tweet
Share
23.2K Views

Join the DZone community and get the full member experience.

Join For Free

Scala has a Try type to functionally handle exceptions. I wrapped my head around that concept by using the excellent Neophyte's guide to Scala by Daniel Westheide. This post will replicate that type using Kotlin.

Background

Consider a simple function that takes two Strings, converts them to integers, and then divides them (sample based on Scaladoc of Try):

fun divide(dividend: String, divisor: String): Int {
    val num = dividend.toInt()
    val denom = divisor.toInt()
    return num / denom
}


It is the caller's responsibility to ensure that any exception that is propagated from this implementation is handled appropriately using the exception handling mechanism of Java/Kotlin:

try {
    divide("5t", "4")
} catch (e: ArithmeticException) {
    println("Got an exception $e")
} catch (e: NumberFormatException) {
    println("Got an exception $e")
}


My objective with the "Try" code will be to transform the "divide" into something which looks like this:

fun divideFn(dividend: String, divisor: String): Try<Int> {
    val num = Try { dividend.toInt() }
    val denom = Try { divisor.toInt() }
    return num.flatMap { n -> denom.map { d -> n / d } }
}


A caller of this variant of the "divide" function will not have an exception to handle through a try/catch block. Instead, it will get back the exception as a value, which it can introspect and act on as needed.

val result = divideFn("5t", "4")
when(result) {
    is Success -> println("Got ${result.value}")
    is Failure -> println("An error : ${result.e}") 
}


Kotlin Implementation

The "Try" type has two implementations corresponding to the "Success" path or a "Failure" path and is implemented as a sealed class the following way:

sealed class Try<out T> {}

data class Success<out T>(val value: T) : Try<T>() {}

data class Failure<out T>(val e: Throwable) : Try<T>() {}


The "Success" type wraps around the successful result of an execution while the "Failure" type wraps any exception thrown from the execution.

So now, to add some meat to these, my first test is to return one of these types based on a clean and exceptional implementation, along these lines:

val trySuccessResult: Try<Int> = Try {
    4 / 2
}
assertThat(trySuccessResult.isSuccess()).isTrue()


val tryFailureResult: Try<Int> = Try {
    1 / 0
}
assertThat(tryFailureResult.isFailure()).isTrue()


This can be achieved through a "companion object" in Kotlin, similar to static methods in Java. It returns either a Success type or a Failure type based on the execution of the lambda expression:

sealed class Try<out T> {
    ...    
    companion object {
        operator fun <T> invoke(body: () -> T): Try<T> {
            return try {
                Success(body())
            } catch (e: Exception) {
                Failure(e)
            }
        }
    }
    ...
}


Now that a caller has a "Try" type, they can check whether it is a "Success" type or a "Failure" type using the "when" expression like before, or using "isSuccess" and "isFailure" methods, which are delegated to the sub-types like this:

sealed class Try<out T> {
    abstract fun isSuccess(): Boolean
    abstract fun isFailure(): Boolean
}

data class Success<out T>(val value: T) : Try<T>() {
    override fun isSuccess(): Boolean = true
    override fun isFailure(): Boolean = false
}

data class Failure<out T>(val e: Throwable) : Try<T>() {
    override fun isSuccess(): Boolean = false
    override fun isFailure(): Boolean = true
}


In case of Failure, a default can be returned to the caller, something like this in a test:

val t1 = Try { 1 }

assertThat(t1.getOrElse(100)).isEqualTo(1)

val t2 = Try { "something" }
        .map { it.toInt() }
        .getOrElse(100)

assertThat(t2).isEqualTo(100)


Again implemented by delegating to the subtypes:

sealed class Try<out T> {
  abstract fun get(): T
  abstract fun getOrElse(default: @UnsafeVariance T): T
  abstract fun orElse(default: Try<@UnsafeVariance T>): Try<T>
}

data class Success<out T>(val value: T) : Try<T>() {
    override fun getOrElse(default: @UnsafeVariance T): T = value
    override fun get() = value
    override fun orElse(default: Try<@UnsafeVariance T>): Try<T> = this
}

data class Failure<out T>(val e: Throwable) : Try<T>() {
    override fun getOrElse(default: @UnsafeVariance T): T = default
    override fun get(): T = throw e
    override fun orElse(default: Try<@UnsafeVariance T>): Try<T> = default
}


The biggest advantage of returning a "Try" type, however, is in chaining further operations on the type.

Chaining With map and flatMap

A "map" operation is passed a lambda expression to transform the value in some form — possibly even to a different type:

val t1 = Try { 2 }

val t2 = t1.map({ it * 2 }).map { it.toString()}

assertThat(t2).isEqualTo(Success("4"))


Here, a number is being doubled and then converted to a string. If the initial Try were a "Failure", then the final value will simply return the "Failure" along the lines of this test:

val t1 = Try {
    2 / 0
}

val t2 = t1.map({ it * 2 }).map { it * it }

assertThat(t2).isEqualTo(Failure<Int>((t2 as Failure).e))


Implementing "map" is fairly straightforward:

sealed class Try<out T> {
    fun <U> map(f: (T) -> U): Try<U> {
        return when (this) {
            is Success -> Try {
                f(this.value)
            }
            is Failure -> this as Failure<U>
        }
    }
}


flatMap, on the other hand, takes in a lambda expression, which returns another "Try" type and flattens the result back into a "Try" type along the lines of this test:

val t1 = Try {
    2
}

val t2 = t1
    .flatMap {
        i - > Try {
            i * 2
        }
    }
    .flatMap {
        i - > Try {
            i.toString()
        }
    }

assertThat(t2).isEqualTo(Success("4"))


Implementing this is simple too, along the following lines:

sealed class Try<out T> {
    fun <U> flatMap(f: (T) -> Try<U>): Try<U> {
        return when (this) {
            is Success -> f(this.value)
            is Failure -> this as Failure<U>
        }
    }
}


The "map" and "flatMap" methods are the power tools of this type, allowing chaining of complex operations together and focusing on the happy path.

Conclusion

Try is a powerful type, allowing a functional handling of exceptions in code. I have a strawman implementation using Kotlin available in my GitHub repo here.

Kotlin (programming language)

Published at DZone with permission of Biju Kunjummen, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Metal and the Simulated Annealing Algorithm
  • Reactive Kafka With Spring Boot
  • Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
  • How To Create a Homescreen Widget in Android

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!