Elena van Engelen
02/28/2025, 7:38 AMloke
02/28/2025, 7:57 AMloke
02/28/2025, 7:57 AMloke
02/28/2025, 7:58 AMResult
have a second type parameter for the exeception type that may be thrown?Elena van Engelen
02/28/2025, 8:12 AMElena van Engelen
02/28/2025, 8:13 AMhfhbd
02/28/2025, 8:15 AMJacob Ras
02/28/2025, 8:23 AMkotlin.Result
doesn't support specifying the error type, it's always Throwable
.
Keep in mind that the built-in one breaks structured concurrency, if you use it with coroutines (because it swallows `CancellationException`s). I wrote down some examples of that here, but the main recommendation I have is to use michaelbull/kotlin-result or the Arrow library's Either
type instead.Bernd Prünster
02/28/2025, 8:26 AMokarm
02/28/2025, 9:29 AMfinally
replacement using also
is skipped when you return
from the error case instead of rethrowing. That's a footgun. The standard finally
doesn't exhibit such error-prone behavior.
In "Handling multiple exception types" you chose a nonsensical example since the two exceptions already form a hierarchy. You can just catch IOException
and match the type like you do in the runCatching
example, making the entire section moot.
In "Handling nested exceptions", again, a bad example was chosen. The try
example is bad code that wouldn't typically be written in the first place. Consider:
your example:
fun processFile(path: String): ProcessedData {
return try {
val content = File(path).readText()
try {
val json = parseJson(content)
try {
return processData(json)
} catch (e: Exception) {
logger.error(e) { "Failed to process data" }
throw e
}
} catch (e: Exception) {
logger.error(e) { "Failed to parse JSON" }
throw e
}
} catch (e: Exception) {
logger.error(e) { "Failed to read file: $path" }
throw e
}
}
How it would be written in the real world:
fun processFile(path: String): ProcessedData {
val content = try {
File(path).readText()
} catch (e: Exception) {
logger.error(e) { "Failed to read file: $path" }
throw e
}
val json = try {
parseJson(content)
} catch (e: Exception) {
logger.error(e) { "Failed to parse JSON" }
throw e
}
return try {
processData(json)
} catch (e: Exception) {
logger.error(e) { "Failed to process data" }
throw e
}
}
Nobody programs like your examples and if they do, they don't pass code review. We don't nest block like this, regardless of whether it's try/catch
or if/else
.
In "Falling Back to a Default Value", again, bad example. Why would you ever catch Throwable when trying to parse an integer? And why wouldn't you use the stdlib String.toIntOrNull()
... in an article about the stdlib?
The article tries to normalize blanket catching of Throwables for no good reason, which is not a good idea unless you have one of those 1:10_000 cases on the boundary of your system.
As it stands, this is slop that reduces the signal/noise ratio, i.e. it is just noise.loke
02/28/2025, 9:36 AMThrowable
. The cases when that is the right thing to do are very few (if they even exist).Bernd Prünster
02/28/2025, 9:38 AMloke
02/28/2025, 9:38 AMloke
02/28/2025, 9:40 AMloke
02/28/2025, 9:41 AM@JvmInline
Bernd Prünster
02/28/2025, 9:41 AMloke
02/28/2025, 9:45 AMokarm
02/28/2025, 9:46 AMloke
02/28/2025, 9:49 AMexpect
to wrap platform functionality, so the implementation looks like this:
expect value class BigInt(val impl: Any)
This means that the actual class being used is going to be java.math.BigInteger
in Java, and a special class wrapping a GMP object in native, etc. This avoids an extra level of indirection, that is pretty important since I can have arrays of billions of these.Elena van Engelen
02/28/2025, 9:51 AMBernd Prünster
02/28/2025, 10:20 AMResult
to not pay the instantiation overhead and the other returns a non-value class. The latter (which also provides a mapper to Result
) we use for APIs that need to be consumable from Swift. Neither breaks structured concurrency as we copied the logic from Arrow to filter exceptions.loke
02/28/2025, 11:19 AMAny
, and then the methods on it would have to use is
to determine what type of object you have, so that it can do the right then. Then the .value()
method would just be a type cast of the wrapped value. But yes, hacky, and certainly something you'd only do if it's really important to avoid the overhead.loke
02/28/2025, 11:20 AMBernd Prünster
02/28/2025, 11:21 AMElena van Engelen
02/28/2025, 2:14 PMBernd Prünster
02/28/2025, 11:40 PMCancellationException
does remedy breaking structured concurrency, having to jump through such hoops speaks against runCatching
. More importantly, there are other Exceptions on the JVM that should never be caught, because when they do get thrown, all bets are off and you would be wise to just terminate the process. See the Arrow sources for a list.
The root of all evil here, though, is not in your article (even in its original version) or whatever coding standards may or may not be violated by the examples, or Result
being a value class which makes it unusable for APIs that should be consumable from Swift (which will still be a requirement for mobile developers). The actual issue here is JetBrains having introduced runCatching
as a public-facing part of stdlib and actively advertising it in the past (do they still?). This discussion here is just a further indicator for how problematic the semantics of runCatching are, which is why it is verboten throughout our codebases (and we only use our own solution, because we don't want every project to depend on arrow). I almost certainly am not impartial, since I am the main author and maintainer of our drop-in replacement for the Result
and runCatching
tandem – but the simple fact that there's over a dozen of such replacements out there speaks for itself. I'm happy to discuss this issue further and I am curious about a couple of things, but this thread is probably not the right place.
Edit: full disclosure - we used runCatching
quite a lot and only by dumb luck avoided real issues. Only later did we find out how problematic runCatching
really is.Stefan Oltmann
03/01/2025, 6:23 AMElena van Engelen
03/01/2025, 7:00 AMElena van Engelen
03/01/2025, 7:01 AM