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@JvmInlineBernd 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