I'm working with Kotlin for some time. Compared to Java it's more concise. Still it has some legacy inherited from Java. For example the try-catch-construct. Though it has been upgraded from a statement in Java to an expression in Kotlin, it's still some kind bulky in writing, assuming we stick to the officially mentioned code formatter and the code conventions regarding the control flow statements.
Then a rather short
fun dayOfWeek() : DayOfWeek {
return try { DayOfWeek.of("1".toInt()) }
catch (e: NumberFormatException) { logAntThrow(e) }
catch (e: DateTimeException) { logAntThrow(e) }
}
becomes a bulky
fun dayOfWeek() : DayOfWeek {
return try {
DayOfWeek.of("1".toInt())
} catch (e: NumberFormatException) {
logAntThrow(e)
} catch (e: DateTimeException) {
logAntThrow(e)
}
}
and breaks the code flow.
Trying to follow the design of Kotlin's run
and let
I wrote this draft:
data class Try<T, B : (T) -> R, R>(
private val it: T,
private val block: B,
private val exceptionHandler: Map<KClass<Exception>, (e: Exception) -> R> = emptyMap()
) : () -> R {
fun <E : Exception> on(e: KClass<E>, handleException: (e: E) -> R): Try<T, B, R> =
copy(exceptionHandler = (exceptionHandler.toMap() + (e to handleException)) as Map<KClass<Exception>, (e: Exception) -> R>)
override fun invoke(): R {
return try {
block(it)
} catch (e: Exception) {
(exceptionHandler[e::class] ?: throw e)(e)
}
}
}
typealias TryRun<T, R> = Try<T, T.() -> R, R>
typealias TryLet<T, R> = Try<T, (T) -> R, R>
fun <T, R> T.tryRun(block: T.() -> R): TryRun<T, R> = TryRun(this, block)
fun <T, R> T.tryLet(block: (T) -> R): TryLet<T, R> = TryLet(this, block)
Motivation:
- Declaring
TryRun
andTryLet
makes the use cases explicit. Additionally it defines the generic type parameterB
ofTry
and provides thus an easier way to use thanTry
itself. - I choose to declare a function named
on
rather thancatch
. In my opinion it reads better within the code flow. It implies the fact of catching anException
but shifts the focus on the corresponding behavior. - The declaration of
on
provides a usage following the builder pattern. But it doesn't change the internal state, it creates every time a newTry
instance and thus doesn't use side-effects. - Using the
invoke
operator we can finish the building process without calling an additional named function. Everything in between is the parametrization of this call.
Some examples following the code conventions on wrapping chained calls and lambdas.
fun main() {
// succeeds - only happy path
"1".tryLet { DayOfWeek.of(it.toInt()) }()
.let { println("Day Of Week is: $it") }
"1".tryRun { DayOfWeek.of(toInt()) }()
.let { println("Day Of Week is: $it") }
// succeeds - provding two cases of exception handling
"5".tryRun { DayOfWeek.of(toInt()) }
.on(NumberFormatException::class) {
log(it)
DayOfWeek.MONDAY
}.on(DateTimeException::class) {
log(it)
DayOfWeek.MONDAY
}()
.let { println("Day Of Week is: $it") }
// fails with DateTimeException - (re)throws the exception
"0".tryRun { DayOfWeek.of(toInt()) }()
.let { println("Day Of Week is: $it") }
// fails with DateTimeException - returns SUNDAY as default value
"0".tryLet { DayOfWeek.of(it.toInt()) }
.on(NumberFormatException::class) {
log(it)
DayOfWeek.SUNDAY
}.on(DateTimeException::class) {
log(it)
DayOfWeek.SUNDAY
}()
.let { println("Day Of Week is : $it") }
// fails with NumberFormatException - returns null as default value
"five".tryLet { DayOfWeek.of(it.toInt()) }
.on(NumberFormatException::class) {
log(it)
null
}.on(DateTimeException::class) {
log(it)
null
}()
.let { println("Day Of Week is : $it") }
// fails with NumberFormatException - explicitly re-throws the exception
"six".tryLet { DayOfWeek.of(it.toInt()) }
.on(NumberFormatException::class) {
logAntThrow(it)
}.on(DateTimeException::class) {
logAntThrow(it)
}()
.let { println("Day Of Week is : $it") }
}
fun log(entry: Any) = println(entry)
fun logAntThrow(e: Exception): Nothing {
log(e)
throw e
}
This is a draft and intended for first feedback. It is e.g. missing support for the finally
case and doesn't take inheritance into account determining an exception handler.
Any hints / ideas ?
2 Answers 2
Consider options
You should consider looking at Kotlin's existing runCatching and the Arrow library's either.
A lot of what you are trying to accomplish already exists either built-in inside Kotlin or in a popular library.
Your code
I think that this code feels weird:
"0".tryRun { DayOfWeek.of(toInt()) }()
.let { println("Day Of Week is: $it") }
And it is clearer to write it as one of the following:
runCatching { DayOfWeek.of("0") }.fold(onSuccess = { println(it) }, onFailure = { throw it })
runCatching { DayOfWeek.of("0") }.onSuccess { println(it) }.getOrThrow()
Additionally, your code creates a bit of overhead by creating instances of Try
and multiple Map
instances for every tryRun
/tryLet
that you do.
Multi-catch
I notice that all your catch
statements seem to do the same thing at the moment, if that will be the case then you might want to consider catching a more generic exception. Even if you want to handle exceptions differently, you could get some inspiration from an article on how you would do multi-catch in Kotlin using the when-statement.
Side-note
As a side note, I think that your comparison of "Then a rather short" ... "becomes a bulky" is a bit unfair, as the first example (the "rather short" one) also doesn't match any Java coding conventions that I am familiar with.
-
\$\begingroup\$ Appreciate your answer. I'm working with Kotlin 1.8. I wasn't aware of
runCatching
. I wouldn't bother to design my draft, knowing that there is already an out-of-the-box solution. It's like reinventing the wheel. The "rather short" code was an attempt to write the try-catch-syntax in a possible shorter way. Since it is already Kotlin code, I wasn't comparing it to Java. That would indeed be unfair. \$\endgroup\$LuCio– LuCio2024年01月07日 16:03:09 +00:00Commented Jan 7, 2024 at 16:03 -
\$\begingroup\$ @LuCio No one is forcing you to follow all the Kotlin code standards everywhere, so you can go ahead and use the "rather short" version if you want to. Be pragmatic :) \$\endgroup\$Simon Forsberg– Simon Forsberg2024年01月07日 16:22:17 +00:00Commented Jan 7, 2024 at 16:22
-
\$\begingroup\$ I've read the "article on how you would do multi-catch in Kotlin". Seems they were also missing a more sophisticated way to handle different exceptions. I wasn't that far from it defining my
on
function. Maybe I would end up with something like that, if I have been aware ofrunCatching
. \$\endgroup\$LuCio– LuCio2024年01月07日 16:45:02 +00:00Commented Jan 7, 2024 at 16:45 -
\$\begingroup\$ @LuCio Yes, using
when
-statements like they do in the article has the benefit of not requiring to instantiate any maps. If you have any other Kotlin concerns that you might need, you can usually find me in this chat room (which is also where I have a feed so that I could find your question) \$\endgroup\$Simon Forsberg– Simon Forsberg2024年01月07日 17:22:34 +00:00Commented Jan 7, 2024 at 17:22 -
2\$\begingroup\$ This question & review couldn't have a better timing. I just needed to validate user input in my desktop app and
Either
solves this so nicely ;-D \$\endgroup\$t3chb0t– t3chb0t2024年01月08日 10:04:57 +00:00Commented Jan 8, 2024 at 10:04
When I started working with kotlin I also had this same problem and came up with the following solution which is an alternative approach to the original question:
/** Try catch function that automatically logs error*/
inline fun safeMode(crossinline body: () -> Unit) {
try {
body()
} catch (e: Exception) {
log(e)
}
}
/** Try catch function that automatically logs error and returns null if exception occurs
* otherwise it returns body expression. */
fun <T> safeReturn(body: () -> T): T? {
return try {
body()
} catch (e: Exception) {
log(e)
null
}
}
/** Try catch suspended function that automatically logs error*/
suspend inline fun safeSuspended(crossinline body: suspend () -> Unit) {
try {
body()
} catch (e: Exception) {
log(e)
}
}
and its usage is also quite simple. by using elvis operator I handle the null cases into default values like:
In case of error it calls an additional lambda which is returning a default object for DayOfWeek.
val dayOfWeek = safeReturn {
DayOfWeek.of("1".toInt())
} ?: {
println("error")
DayOfWeek.defaultObject()
}
// It returns string in both cases. In case of error it returns hard coded string 'Monday' basically a similar implementation to the above safeReturn code but without lambda.
val dayOfWeek = safeReturn {
DayOfWeek.of("1".toInt()).toString()
} ?: "Monday"
// In case of error, It returns null which triggers elvis operator to call the second lambda code.
safeReturn {
DayOfWeek.of("1".toInt())
} ?: {
println("error")
}
// try catch but without the catch and just logging the exception.
safeMode {
DayOfWeek.of("1".toInt())
}
-
\$\begingroup\$ Indeed, as you said a quite simple solution which looks very handy as long as you don't need to handle a thrown exception (apart from logging it). \$\endgroup\$LuCio– LuCio2024年04月18日 15:08:57 +00:00Commented Apr 18, 2024 at 15:08
-
\$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$Toby Speight– Toby Speight2024年04月20日 14:55:13 +00:00Commented Apr 20, 2024 at 14:55
-
\$\begingroup\$ I think all of the above mentioned codes perform almost same functionality OR work towards solving the same issue. However, my code is more simpler in usage and in understanding. I've edited it nonetheless. \$\endgroup\$Haseeb Hassan Asif– Haseeb Hassan Asif2024年04月22日 04:53:31 +00:00Commented Apr 22, 2024 at 4:53
Explore related questions
See similar questions with these tags.
logAntThrow
... those poor ants. \$\endgroup\$