kotlinx.coroutines: Provide a `runCatching` that does not handle a `CancellationException` but re-throws it instead.
Problem Description
The Kotlin StdLib contains a function named runCatching. It tries the provided lambda and catches any Throwable that is not caught or thrown by the lambda.
However, using runCatching in Coroutines can have a somewhat hidden (and unexpected) side-effect of it catching CancellationExceptions and not re-throwing them. This could interfere with the Coroutines’ Structured Concurrency, with its cancellation handling.
Possible Solution
E.g. create a new function in kotlinx.coroutines that looks something like this (not sure about the name 😄):
public inline suspend fun <R> runSuspendCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch(c: CancellationException) {
throw c
} catch (e: Throwable) {
Result.failure(e)
}
}
The runSuspendCatching will catch any exception and wrap it in a Result, except for CancellationExceptions that will be re-thrown instead. This will prevent unwanted interference with the cancellation handling of Coroutines.
Notes
- The
suspendkeyword is added to prevent this new function from being called in regular non-suspending/blocking code. - When code calls the StdLib’s
runCatchingin a Coroutine/suspendable-context, a lint warning should warn the developer that handling cancellations may be compromised andrunSuspendCatchingshould be used instead.
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 94
- Comments: 71 (25 by maintainers)
runCatchingis expected to run catching. It would be very strange if some exceptions would not be caught by it. I can imagine the usefulness of a new functionResult<*>.except<reified T : Throwable>()orResult<*>.throwIfIsInstance<reified T : Throwable>()that would rethrow. For example:It would allow you to:
My use case is that I materialize various errors from lower layers in an Android view model for its observers and I do some logging. This just so happens to be in a coroutine context (in suspend funs running in the view model coroutine scope), so this might throw
CancellationExceptionat any time. I don’t want to catch and materialise those, so in my use case I just want to rethrow them so that all parents can handle them too: they might need to do some finalising before they can cancel properly.Indeed. I understand that Flow is in the coroutine world and
runCatchingis not, so it somehow makes sense. But it also makes things more confusing that one of them is “cancellation aware” while the other is not.@qwwdfsad
If the developer uses the StdLib
runCatching { ... }and therunCatchingwraps a suspendable piece of code (i.e. it could throw aCancellationException), therunCatchingshould re-throw anyCancellationException. If not the suspendable caller ofrunCatchingmay not get cancelled and the Structured Concurrency of Coroutines breaks down.@fvasco Good question; maybe 😃 But this is about CancellationExceptions when using runCatching in a Coroutine.
@qwwdfsad Here is an example of where using
runSuspendCatchingcan fix the cancellation-handling issue caused byrunCatching:Running the above
mainfunction prints out:This is not right. The
screenScopewas cancelled, the call towriteToDatabaseshould not have happened. But it did happen, because therunCatchingdid not re-throw the CancellationException thrown by thedelay(1000)statement.If we change the code like this (replacing
runCatchingwithrunSuspendCatching):this is printed out:
This is correct. The call to
writeToDatabasewas not made at all, which is the desired behavior (try replacingrunSuspendCatchingwith just a plainrunand remove thegetOrElse; you’ll get the same result).Bad news: there’s no version of
runCatchingthat correctly supports what the coroutines library currently has.Here’s some code that shows the issue: https://pl.kotl.in/sgZzeKnJP
In essence, because of https://github.com/Kotlin/kotlinx.coroutines/issues/3658, it’s not correct to just assume that getting a
CancellationExceptionmeans that the current coroutine was cancelled: an operation (for example,Deferred.await) could throwCancellationExceptionjust because it stored one from another coroutine. In this case, the way to go is to check the current coroutine for cancellation, and if the coroutine isn’t cancelled, it means theCancellationExceptioncame was just rethrown from some other coroutine. The recommended pattern if you absolutely must catch every exception (and you typically don’t!) is:It’s also not correct to assume that a
CancellationExceptionis safe to ignore if the current coroutine was not cancelled. For example,Flowcan throwCancellationExceptionfromemitto signal completion, but the current coroutine is not cancelled. The recommended pattern if you absolutely must catch every exception (and you typically don’t!) is:Thank you Anton, really good suggestion.
I have a question: how to deal with the TimeoutCancellationException? In most cases, the timeout should be treated as a failure result. But TimeoutCancellationException inherits from the CancellationException, so it will be re-thrown.
The following example fixes that behaviour:
What do you guys think about this?
The method should just be deprecated. Not only is it unusable in coroutines, but it catches all Errors including VMErrors like OOM/StackOverflow and ThreadDeath.
Using
runCatching {}is tempting to handle the I/O errors described in https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07. There are some cases where listing all possible exceptions is complex, especially when they come from deeply nested, potentially JDK/third party SDKs.It’d be nice to be more explicit as to how to use exceptions. In the current state, APIs like
runCatching {}andFlow.catch {}make it easy to use exception for control flow which can be dangerous as showcased here.PS: also, the fact that
Resultwas only available fromrunCatching {}for some time was an incentive to use the dangerousrunCatching {}A newbie shouldn’t catch all exceptions, that’s the bad pattern to avoid, and easy to spot. Coroutines
CancellationExceptionis just one example of an exception you shouldn’t have tried to catch.Any updates on a possible solution or alternative? From my use cases, what I really need is a good way to try-catch inside suspend functions.
1. Using
runCatchingandonFailureDrawbacks:
ThrowableCancellationException2. Using the language
try-catchDrawbacks:
CancellationException3. Using the
flowAPIDrawbacks:
Throwableflow,catch,emit,singlepattern)My take on this
In my opinion, neither of these options is good enough and especially hard and error prone to newcomers. I don’t really want them to catch a
Throwableand I don’t want to repeat the re-throwing logic everywhere.Also, when looking for the term
kotlin coroutine exception, most of the articles you find on the google first page contain examples of using the languagetry-catchbut they don’t really bother re-throwing theCancellationException(which is what I usually see in the newcomers code).It seems that the KEEP’s use case ‘Functional error handling’ (specifically the example with
runCatching { doSomethingSync() }) is one that easily introduces the side effect being discussed in the current issue: it catches all throwables instead of the ones that are expected.The proposed idiomatic solution in that KEEP is to be explicit about any expected outcome: either always return a result (but don’t throw), or return a nullable result (but don’t throw), or materialise various outcomes, e.g. use sealed types of expected failures or results (but again don’t throw). If applied ad nauseam, then the KEEP is right: you would only expect exceptions from the environment (e.g. out of memory) or from failure-heavy operations or libraries (e.g.
CancellationException).Are the aforementioned use case and the proposed ways of handling errors in the function’s signature contradictory?
@Anton-Spaans-Intrepid Let’s take a look at a more realistic example, please. What are the use-cases for
runCatchingin an application code? Can you give a code snippet that motivated you to come with this proposal?FWIW, the
catchoperator ofFlowalready specifically handlesCancellationExceptionin that it will re-throw it if theThrowableinstance is of typeCancellationException.I also asked about a similar
runCatchingvariant forsuspendfunctions in https://medium.com/@mhernand40/thank-you-for-the-reply-dd01cf846ab7.Off-topic. Shouldn’t there be a warning in
runCatchingdocumentation? Yes, the code of this method is pretty simple, but you need to mindfully read it to understand the problems it may lead to. Maybe some annotation like@DelicateCoroutinesApi(just an example, I knowrunCatchingis not a part of coroutines API) will prevent erroneous and unconscious usage of this function?After Kotlin 1.7 underscore operator for type arguments, @erikhuizinga solution is even more appealing because you can use it that way:
the type of the item inside the result will be automatically inferred by the underscore, without the need to use
*.Also, what’s with OutOfMemoryErrors and similar? It’s an antipattern (not sure whether it’s disputed or not) to catch
Throwablein Java, and I’d guess it also is in Kotlin.Example: I don’t think that it’s really expected by the user to ignore OutOfMemoryErrors and NOT dump the JVM heap, even if you configured the JVM to dump. Catching Throwable will do just that (again: Not 100% sure about Kotlin, not the expert, yet).
100% disagree.
Very simple usecase : I do a GET call on Ktor and I don’t care at all about any exception. I just want the
.body()deserialized as my data class or null, with a 0% chance of crash, now or later. You can’t achieve that withoutcatch (e: Exception).Unfortunately, no. There are two conceptual problems we see here:
It also would be nice to see more real-world use cases for such API to understand what kind of problems are being solved. One can argue that “it’s obvious that we don’t want to catch CE”, but it does not help to shape API. For example, if the majority of cases operates with
Resultin a declarative manner, thenexcept<>extension might do the trick. For others,runSuspendCatchingmight be preferable and it’s crucial for us to understand what exactly is being solved.The compiler knows nothing about kotlinx coroutines, neither it knows about
CancellationExceptionand we would like to avoid pushing particular library concept into the core of the language@vogthenn by that reasoning I wonder what the original use case is for
runCatching, because you make it sound like an antipattern without legitimate uses.Also, just to play devil’s advocate, there has been requests to provide real-world examples of where you would want to rethrow a CancellationException on runCatching but what about some real-world examples of where you would not?
Looking through all the runCatches in suspending functions in the my codebase, there isn’t a single one where it would make sense to catch the CancellationException.
I think it’s fair to say the current default behavior != typical use case. If runCatching was being introduced to the language now I think it would be wise for it not to catch CancellationExceptions by default. But alas it’s a stable API and set in stone now, a bit like all the Java relics that made us switch to Kotlin in the first place. Perhaps it will take a new language with native coroutine support to solve this one? I propose the name Colin 😆
@Anton-Spaans-Intrepid Do you think that the current
runCathingfunction should rethrow anyInterruptedException?My two cents here about real world examples…
As an Android dev, when I am doing an http request, I don’t usually care if it was an HttpException, an IOException, a ParseException or a NullPointerException. My operation failed. All I need to do is to log the error in the report tool and update the UI with the error state. One exception that I really don’t need to log in the report tool is CancellationException, I also don’t need to show an error in the UI because of that, but I do would like to have my parent coroutine scope cancelled properly and avoid memory leaks (I understand that, I may have multiple http requests ongoing, the user leaves the screen, the parent scope asks to be cancelled, the first coroutine scope will try to be cancelled, not cancel because of the current behaviour runCatching method, any other ongoing requests will not be cancelled and I will have a memory leak while those requests don’t finish).
To avoid this real world scenario, we are using something similar to
suspendedRunCatchingin our whole codebase that uses coroutines.@SSong-develop can I rewrite your example?
What should return
runSuspendCatching?This issue includes the same problem of #3658. Catching a
CancellationExceptiondoes not implies that the current job has been cancelled. So arunCatchingshould include anensureActive, because aCancellationExceptioncan imply the cancellation of the current job. However, therunCatchingcan produce aResulteven if the current job has been cancelled, sorunCatchingshould executerunCatchingin any case? In other words: doesrunCatchingcheck the cancellation of the current job?Suspending functions check the cancellation only under some conditions (see
delay), so I think that this task should not be included inrunCatching. In other words: running in a cancelled scoperunCatching{ delay(0) }should return a success or a failure?In #3658, in after each
catch(Throwable)should be executed anensureActive(), similarly after eachrunCatchingshould be executed anensureActive().Any update on this ?
Being able a catch a
CancellationExceptionin a coroutine doesn’t make any sense to me. In a perfect world, it should be impossible. It only happens because coroutines are based on Exceptions to propagate state. To me, it’s an anti-pattern for a coroutine to be able to prevent the termination of its scope. A coroutine can specify it needs to “terminate gracefully” with afinallyand / or withNonCancellable, but that should be it.Why not instruct the compiler to automatically re-throw the CancellationException in a catch block surrounding a suspending function ? I can’t see a decent usecase where this wouldn’t be beneficial.
To be honnest, I expected this behavior to be automatic since there’s so many bytecode generated for coroutines. I expected the “magic CancellationException propagation” would be somewhat “protected” at the compilation level when generating the code behind coroutines.
@NinoDLC well for the record
catch(e: Exception)doesn’t achieve 0% chance of crash either, anyErrorthrown will break this, such as OOM,ThreadDeath,StackOverflowError, etc. It’s kind of a fallacy to expect that you can avoid crashes this way. The code in your catch block can probably throw as well.Also, are you returning
nullin case ofClassNotFoundException? Don’t you need to distinguish this from a 404? Don’t you need to distinguish a 404 from a 503 or 500? You probably want to respectively return null, retry, or show the user an “internal error on our end” kind of message. Certainly the code handling the general error message display will not be next to thehttpClient.get()(probably a couple architecture layers above, so the exception bubbling is what you want here). So I would repeat,catch(Exception)would be the newbie mistake in this case, and probably should be caught during code review, along with theCancellationExceptionproblem.The use case described by @Legion2 fits with the intended use case for
runCatching: postponing failure (as pointed out in this earlier comment: https://github.com/Kotlin/kotlinx.coroutines/issues/1814#issuecomment-763908770). Handling (i.e. deliberately swallowing or rethrowing) the caughtCancellationExceptionfurther down the line is ok, as long as it happens. However, one might argue that catching everything (i.e. what you expect and do not expect) is an anti-pattern, because how do you communicate what exceptions can be caught here that are to be handled down the line? But it is a use case.The alternative pattern really only is to handle everything you expect: normal returns and expected exceptions. When calling any suspending function (i.e. a suspension point that can resume normally, or with normal cancellation or failure), you should handle cancellation. The default case, i.e. not catching anything and thus propagating the cancellation up to the parent job, is a brilliant design choice of the kotlinx.coroutines lib authors.
On the topic of this issue: while I’m probably ignorant of many aspects to consider, would it be possible to introduce a linter warning/error that informs you that you’re catching cancellation, but not deliberately handling it? One might argue that this should be somehow marked with
@DelicateCoroutinesApi, because the use cases are limited (as @nbransby pointed out) and it’s easy to get it wrong by swallowing cancellation. And/or maybe introduce a suspending variant ofrunCatchingthat would clearly document these details.They use this
suspendRunCatchingprivate function only once in the file, and only checkisSuccesson the returned result. They could technically just catch and return false in case of failure. There is no need for aResulttype here.Also, they catch
Exception, which is better than catchingThrowable, but then it’s not really similar torunCatchinganymore, so I guess they should rename it if they opt for the boolean return type, or they could even inline the function and delete it.The Now In Android app which for Android developers should be the go to best practice example has an example of following this pattern here https://github.com/android/nowinandroid/blob/607c24e7f7399942e278af663ea4ad350e5bbc3a/core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt#L57
This app is also on the Google Play Store. Any recommendations of how they could improve or is this a valid usecase?
I see.
Then I got quite a simple question [to everyone interested in this API]: why do you prefer
runCatchingthat ignoresCancalletionExceptioninstead ofrunCatchingthat catches all, but invokesensureActiveright after? If there is any difference, why it does matter for you?@erikhuizinga https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md Looks like intended use cases are postponing failure and internal within coroutines.
nullshould be used when possible for recoverable errors, and exceptions should be used for programmer bugs. If your program is in an incorrect state, it’s incorrect for it to continue executing, better to notify the programmer about their mistake and restart the program, or who knows what else the program may do? Kotlin’s core libraries throw exceptions all the time when it’s expected that the programmer was wrong, returningnullinstead when it’s expected that the programmer can reasonably react to the error. Often, both throwing and null-returning versions are provided to support both kinds of use cases.You can’t magically achieve good, Kotlin-idiomatic error-handling by wrapping an exception-based API into a
runCatching, because that would mean saying, “I can handle all exceptional situations, I guarantee that this code always behaves correctly, no matter what exception it’s failing with.” And that is a really tough thing to guarantee! When presented with several exceptions that you want to wrap in a Kotlin-idiomatic way, you have to decide which exceptions you can react to and which indicate an exceptional condition. Otherwise, you get things like 100% CPU consumption while waiting in a loop for your network connection to restore, or you get silently swallowed indicators that your Wi-Fi adapter has broken, or something like that: there very often are things a program can’t meaningfully react to, and it’s desirable to use exceptions in such cases and crash (or at least log) everything.EDIT: yep, see the section “Handling program logic errors” from the article you linked.
cc @e5l: hi! In the messages above, @NinoDLC says that Ktor throws too many exceptions even when the Kotlin-idiomatic way would be not to fail but return something to let the programmer deal with it, forcing the user to wrap code in a
catch (e: Exception). Maybe this feedback could be actionable (or maybe there are already ways to work around that).It is possible to omit the underscore if such function is introduced as a member function of Result:
If I’m not mistaken, it won’t become the actual member in the bytecode due to
@InlineOnly.This is a perfect example: https://github.com/Kotlin/kotlinx.coroutines/issues/1814#issuecomment-588317835. Why isn’t it more considered ?
Even if @elizarov thinks this is not a realistic use case, this is exactly the kind of problem I’d meet in Android if I tried to protect my project from random Exceptions being thrown from a library I can’t control (or ditch 😄 ).
runCatching(or just plaintry / catch Exception) is exactly this kind of use-case, to me. Transforming a world of obscure / goto Exceptions to a world of exhaustive sealed classes.@elizarov I also have this use case. My repository is private so I can’t post the source, but just in general I’ve had to repeat the same pattern where I want to handle some exception in a suspending function somehow. For example I can log, or I can send over to a crash reporter. However, what if this exception is handling is a
CancellationException? I think in most cases you want to ignore this exception when logging or your crash reporter.With
CancellationExceptionI’ll want to rethrow it or just ignore it. It would be nice to just have somerunCatchingfunction that does this behavior for you. You can of course write it pretty simply yourself (that’s what I ended up doing), but I think it’d be nice if the library included it for you. I think in general there’s a lack of documentation on best practices on handlingCancellationExceptionfrom the library. Maybe you can include such a thing and also link to this utility function?Sorry if that’s the case, I’m genuinely trying. I’m literally quoting your own sentences without paraphrasing when I reply, to avoid misunderstandings. On the other hand, it seems you really want to read “let the app crash” when I write “catch at a higher level”, so it looks like you’re making a strawman out of my arguments. Let’s try to stick with what we’re actually writing, so we don’t put words in each other’s mouths, because I agree this doesn’t feel nice.
I totally agree with this, but then you prone catching everything including programming errors, which is a contradiction (or a tradeoff, but you didn’t present it as a tradeoff). In your option 1, you don’t mention it but you’re catching all of these too. Also, you write type safety / no type safety in the options, but returning
nullfor everything doesn’t give information about anything, just like usingAny?everywhere doesn’t bring any type safety. On the JVM any call can fail with a throwable, that’s an implicit part of any function signature. Using null doesn’t change this. Proper type safety would be to use a sealed type for the errors, but then won’t you complain about the boilerplate to handle all these errors in every call?Also, those are definitely not the only 2 options. You don’t seem to account for my suggestion anywhere: return null for what makes sense to be represented as null, let programmer errors bubble up (0 boilerplate), don’t catch generic exceptions at a low level, let them bubble up but catch them at a higher level in your app to avoid app crashes and report them accordingly (in framework/architecture code, not business code, because we don’t want this boilerplate there, I agree).
Catch the
IOExceptioneither on the spot or at a level that makes sense for this call? I mean, I’m only saying not to catch a generalExceptionat a low level without knowing what you’re catching (again, please do handle them at a higher level, I’m not saying to let the app crash).Also, feel free to use extensions. I don’t think it’s valid to blame the library API design for your decision to use
catch(Exception)in business code. You’re saying that all your calls to Ktor should be wrapped incatch(Exception)due to poor Ktor design, yet you’re also writing “I’m only speaking about “exceptions that aren’t””. If the API doesn’t suit your needs, you can definitely write your own extensions on top of Ktor’s functions, and use those instead. I still reckon those extensions should catch just what you need to catch - the infamous “exceptions that aren’t” as you put it -, and you could turn the result into sealed types for instance. So these extensions would still not requirecatch(Exception)IMO, because you still want IAEs, ISEs, etc. to bubble up (at least that’s what you wrote earlier too). Even if you did catchExceptionin general in these extensions, those are no longer business code, and I’m almost fine with those. But you’re talking about puttingcatch(Exception)throughout your code, which is different.(NB: I’m assuming you mean
String.toIntOrNullwhen writingInt.parseOrNull())I’m not sure about the point you’re making here. These functions don’t claim to never throw, they only claim to return null in specific documented cases. The first 2 return null when the input is not valid, the 3rd one returns null when the index is out of bound. If you look at the code,
String.toIntOrNull()doesn’t catch any exception at all, andDuration.parseOrNullonly catches a specific exception too. Both of those could definitely throw exceptions in case of other issues. They don’t contain acatch(Exception)guard or anything, and that’s not the point.Also,
toIntOrNull()is not more idiomatic Kotlin thantoInt()(which seemed to be your point). It’s all about expressing a different intention:toInt()means you don’t expect an invalidStringhere, and it would be a programmer’s error to let one arrive at that point, whereastoIntOrNull()explicitly expects invalid input and deals with it by handling the null result. Ktor offers the same choice by providingexpectSuccess = true|false. The case forIOExceptionis indeed debatable, but that’s an orthogonal discussion I believe.Totally agree, let’s not forget our heritage! But in my opinion there should be APIs in Kotlin to handle this problem, like nullability, reified generics, smartcasting and other has been handled by the Kotlin compiler to provide a better coding experience when needed / possible.
Moreover, Ktor should provide a “kotlin way” to use its APIs. This is the only lib I have to try/catch.
@dkhalanskyjb and @joffrey-bion You completely missunderstand my points almost like a strawman, I’m only talking about environment-induced exceptions, not logical (programming) errors or even
Errors. Obviously even in my code I’llrequire()some parameters to not be completely illogical. Obviously in these cases I want my program to crash because it’s becoming illogical anyway, so better now than later.Between:
1/ erasing all environment-induced exceptions “information” to
null+ not having to worry about a rogue crash for some random thrown exception I don’t care + type safetyand
2/ fine grained and boilerplate approach to
try / catchexception handling + occasionnal crashes because of missing documentation (or bumping a version that introduce a new thrown exception for example…) + no type safetyI, personnally, as an application developper, choose 1.
Obviously, for some low level or high complexity codebases, at one point, you need solution 2/. I understand that. But the choice should rely on the developper.
If I don’t catch everything comming from Ktor, my end-user application will crash because their phone was on the subway and couldn’t reach the server, triggering an IOException. What do you propose then ?
Once again, I’m only speaking about “exceptions that aren’t” in the sense they’re not “exceptions”, in the end they are merely expected possible failures because, once again, a mobile device is mobile and its internet connection is unstable.
Ktor yes. It feels not Kotlin at all in its call APIs. Why can’t I have a simple
getOrNull()/postOrNull()/ etc… like we haveInt.parseOrNull(),Duration.parseOrNull(),List.getOrNull()and so like we do in the rest of the Kotlin ecosystem… I’m not saying this is the only APIs that should be available for Ktor, because obiously we need some fine grained approach for at least telling the difference between a connectivity error and a server error. And in this case, I imagined they would be available as a type-safe model (sealed classes and others) instead of exceptions.Like the nullability feature in Java (the “billion dollar mistake”), I feel like Kotlin, because of interoperability with Java, didn’t “fix” the exception problem and to this day, even in Kotlin, we still use exceptions to drive our code flux instead of type safe alternatives.
OFF:
I agree but currently there are lots of issues caused by the “fundamental design” of the cancellation implementation. There are related problems/issues/questions (like this one) showing that even advanced users are sometimes confused how to correctly handle errors. And if you are a very advanced user (or even a contributor), please think with the head of less advanced ones 😉 (And error handling is the key of successful software, nowdays the “happy path” can almost be programmed by chimpanzees with the help of IDEs, AIs, etc. 😃 )
Another huge problem is that it seems that the coroutines library does not seem to fix the fundamental issues because of backwards compatibility, which is a bad decision IMHO. Why not release a 2.0 version with enhanced APIs, especially in the area of error handling?
It would be awesome! Especially if this documentation would contain the known issues and their “workarounds” as well.
So you can’t have 0% crash, that’s my point. Why did you suggest this as a goal at all?
Because all of those things require a (different) bugfix (and I didn’t say 400 initially, but yeah 400 needs a bugfix too, while 404 may or may not). In your first paragraph, you seem to be ok with crashes if they result from developer mistakes (which I agree with!). Then why would these cases not count? In the same vein, why would you catch
IllegalArgumentException, which is a developer mistake too?Nobody is saying this. My point is that exception handling allows to catch different types of errors at different levels. I certainly don’t want the app to crash, and that’s why you can add higher-level / more generic error handling at different levels thanks to exceptions. I also certainly don’t want a low-level HTTP call to return
nullfor every error. How would I know I need to report this to my metrics (and fix the things that are bugs) if the bugs yield the same result as “there is no value right now”?There are cases for catching
Exception, but usually this implies rethrowing a more specific one with a cause (that’s ok). But here we’re talking about returning null, which is a big no-go for me.Using
runCatching+Resultmanagement adds boilerplate, exception bubbling is free of syntax (it’s the default). Also, you cannot compare the amount of boilerplate of therunCatchingapproach with multiplecatchblocks handling different errors, because these don’t do the same thing. If you have one generic catch-all (which kinda emulates therunCatchingway), then you can compare, but the whole point is that this block wouldn’t be at this level, so again you wouldn’t see the boilerplate.I think you have misunderstood something in the article you linked. Let me quote it: “As a rule of thumb, you should not be catching exceptions in general Kotlin code. That’s a code smell. Exceptions should be handled by some top-level framework code of your application to alert developers of the bugs in the code and to restart your application or its affected operation. That’s the primary purpose of exceptions in Kotlin.”
This is almost the opposite of “you should catch everything and return null instead”.
Do you have an example exception-based API that you consider bad?
@joffrey-bion There’s nothing to do against
Errorby definition : the machine is on an unspecified state (like as you said, process is dying or strack overflow) and any new line of code won’t probably work anyway. Even the Java documentation clearly states we shouldn’t catch it:An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.. And that’s ok if the code crashes there. Either there’s a mistake on the developper part or the process is dead anyway, so who cares ? The crash will appear on the crash tool we use and we can fix the bug.On the “expected exceptions” part, as I said, I don’t care if it’s 400 / 500 / parsing error. I don’t want my code to crash for something I don’t care. That’s quite “pedantic” of some API to say “your application should crash if you don’t handle one the these terrible (undocumented) exceptions that has happened” (which is just an unreachable server because mobile devices are… mobile ?). The call just failed and that’s alright for my usecase. Imagine I just want the temperature from some domotic HTTP device. Why should I care if it’s 400 / 500 / parsing ? No need for extra boilerplate to handle an exception I don’t see value in. That nightmare was some old Java stuff, it’s 2024 now in Kotlin. We can do better. If it doesn’t work, I don’t get the result and I have a compile-time check (nullability / type safe) to ensure it. Much better. Much simpler.
The whole Kotlin ecosystem is based on the nullability of a return to check if it succeeded or not instead of Exceptions. Roman wrote an entire article about it.
Exception handling is still a miserable experience in Kotlin by design as Roman explained: use nullability, or sealed classes if you want fine-grained approach instead.
Based on Kotlin paradigms, we can wrap callback based APIs (bad) to Coroutines (good). We can transform listeners (“bad”) to Flows (good).
But why can’t we easily wrap exception based APIs (bad) to null / type-safe (good) ? You even say we shouldn’t, which I don’t understand why. Except for Ktor, I don’t see any Exception we should try to catch from calling Kotlin libraries, and that’s a huge improvement over Java in my eyes.
It always depends on the application, how one would handle different types of exceptions/errors. So there is no solution that fits them all. I think the best here is to create an excessive guide/docs on exception/error handing in kotlin, including coroutines and also examples from common kotlin libraries such as ktor. So users understand the available error handling features and limitations provided by kotlin and prevent miss use and bugs.
@nbransby Actually I don’t think the use case question was ever about why rethrowing
CancellationException. It’s rather about the need forrunCatchingat all in business code in the first place.I’m specifically talking about business code here because people seem concerned about what many developers would do in regular code, and about repeating snippets etc. If this is about coroutine-aware framework code, then thinking about when to use
ensureActive(or re-throwingCancellationExceptionmanually) should probably be common practice, and there shouldn’t be much repetition there. So having an internal API defined locally in this framework code is probably enough to deduplicate the few usages.Thanks @erikhuizinga for summarizing how I feel. I would actually really like to see
runCatchingmarked as delicate API that requires opt-in. A lot of people use it as a means of switching to a railway programming paradigm, but using theResulttype in business code, which is not the intended purpose.Good summary @fcoutinho-uolinc
Remembering to rethrow CancellationExceptions is a real gotca and too easy to miss. This would be a great thing for a linter to pick up, do any of them support that?
I think it would be hard to introduce a new API thats a non-breaking change that we’ll all remember to use over runCatching or try catch without a rethrow of CancellationException.
If I am not mistaken, the OOM error won’t crash the application if you catch this error. In the old days, we were caching OOM errors during image load and recovering in some way (reducing quality, for example) in case no more memory left. I prefer to leave them untouched.
In some applications, we need to deal with poorly written libraries each day. A typical case is to disable some functionality instead of crashing the application if some poor provider fails. It is a very common problem; our code is full of
try-cachewithif (e is CancellationException)simply because we don’t know a good name for the function.runCachingis a bit too aggressive.Great post!