Nino
03/20/2023, 3:35 PMrunCatching
is in use in any of your Kotlin (or Android) projects ? I feel my team is abusing it and I'm trying to understand if there's any "pros" about using it in any coroutine-based codebase. My team just adopted this convention and I'm at loss of words.
(Examples in thread for anyone interested)Nino
03/20/2023, 3:35 PMclass FetchFooUseCase @Inject constructor(
private val synchronizeFooUseCase: SynchronizeFooUseCase,
private val fooRepository: FooRepository,
) {
suspend fun invoke(fooId: String): Result<ShowEntity> = runCatching {
runCatching { fooRepository.getFooById(fooId) }.getOrNull()
?: (synchronizeFooUseCase.invoke(fooId) as RefreshFooFromServerResult.Success).foo
}
}
which is used by :
class GetBarsByFooIdUseCase @Inject constructor(
private val fetchFooUseCase: FetchFooUseCase,
) {
suspend fun invoke(fooId: String): Result<List<BarAggregate>> =
runCatching {
fetchFooUseCase.invoke(fooId).getOrThrow()
.mapNotNull { runCatching { createBarAggregate(it) }.getOrNull() }
}
}
Another example with Flows :
class GetFooUpdatesUseCase @Inject constructor(
private val fooDownloadStatusRepository: FooDownloadStatusRepository,
private val fooFavoriteRepository: FooFavoriteRepository,
private val fooProgressRepository: FooProgressRepository,
) {
fun invoke(): Flow<FooUpdate> = merge(
runCatching { fooFavoriteRepository.getFavoriteFooUpdates() }.getOrDefault(emptyFlow()),
runCatching { fooProgressRepository.getProgressUpdates() }.getOrDefault(emptyFlow()),
runCatching { fooDownloadStatusRepository.getDownloadStatusUpdates() }.getOrDefault(emptyFlow()),
)
}
Joffrey
03/20/2023, 3:36 PMrunCatching
could be useful, and no business code should use it.Joffrey
03/20/2023, 3:37 PMCancellationException
, which would be incorrect 99% of the timestreetsofboston
03/20/2023, 3:38 PMNino
03/20/2023, 3:38 PMstreetsofboston
03/20/2023, 3:39 PMEither
class instead, where this is made easier and works well within the structured concurrency model of coroutinesNino
03/20/2023, 3:40 PMEither
obviouslystreetsofboston
03/20/2023, 3:40 PMNino
03/20/2023, 3:41 PMJoffrey
03/20/2023, 3:41 PMstreetsofboston
03/20/2023, 3:41 PMNino
03/20/2023, 3:42 PMJoffrey
03/20/2023, 3:43 PMrunCatching
makes things in the first examplestreetsofboston
03/20/2023, 3:43 PMJoffrey
03/20/2023, 3:44 PMNino
03/20/2023, 3:44 PMrunCatching
makes me afraid of "blank pages" where if you have any issue, instead of crashing (which is automatically reported back so we have metrics), you just have a non-functional UI (which is wayyy harder to detect).streetsofboston
03/20/2023, 3:45 PMCLOVIS
03/20/2023, 3:50 PMrunCatching
: it is NOT safe to use with coroutines. runCatching catches Throwable, which includes CancellationException. This stops coroutines from dying correctly: if a coroutine is cancelled, it will still be considered alive by its parent, leading to memory exhaustion and an eventuaal live-lock.Joffrey
03/20/2023, 3:51 PMCLOVIS
03/20/2023, 3:52 PMNino
03/20/2023, 3:52 PMunless it's the UI-state you're modelling, where you fold both errors and good-data into one UIYes that was that strategy we used before: • either a nullable type is returned if we didn't care about the error type (it just "failed", so
null
is returned), so we would have a generic error message displayed in the view
• either a sealed class providing the result or the error (to be able to differentiate between "NotFound" and "NoInternet" errors for example)Nino
03/20/2023, 3:53 PMCancellationException
but it still seemed like a bad idea. Any thoughts ?Joffrey
03/20/2023, 3:54 PMrunCatching
all over the place, specifically using runCatching
at different levels in the code (effectively nesting them, locally or less locally), shows laziness on the part of the devs IMO. It shows that instead of reflecting on the proper place to handle and report errors, they want to "feel" safe and hide errors. This doesn't work in the long run.CLOVIS
03/20/2023, 3:54 PMstreetsofboston
03/20/2023, 3:54 PMCLOVIS
03/20/2023, 3:55 PMstreetsofboston
03/20/2023, 3:56 PMNino
03/20/2023, 3:56 PMstreetsofboston
03/20/2023, 3:56 PMstreetsofboston
03/20/2023, 3:57 PMstreetsofboston
03/20/2023, 3:57 PMNino
03/20/2023, 3:57 PMkotlin.coroutines.cancellation.CancellationException
? That's weirdstreetsofboston
03/20/2023, 3:58 PMJoffrey
03/20/2023, 3:58 PMPeter Farlow
03/20/2023, 3:58 PMpublic actual typealias CancellationException = java.util.concurrent.CancellationException
Peter Farlow
03/20/2023, 3:58 PMstreetsofboston
03/20/2023, 3:58 PMCLOVIS
03/20/2023, 3:58 PMNino
03/20/2023, 3:59 PMstreetsofboston
03/20/2023, 4:00 PMCLOVIS
03/20/2023, 4:01 PMJoffrey
03/20/2023, 4:02 PMTimeoutCancellationException
(subclass of CancellationException
) is that in this case, the exception is an actual error (it expresses a timeout), and it's not meant to cancel the current coroutine. So if the purpose of the over-confident runCatching
is to catch "errors", this timeout should be one of them. Just re-throwing it kinda defeats the purpose (NOT of withTimeout
, but of runCatching
in this case)streetsofboston
03/20/2023, 4:02 PMJoffrey
03/20/2023, 4:03 PMstreetsofboston
03/20/2023, 4:03 PMJoffrey
03/20/2023, 4:03 PMJoffrey
03/20/2023, 4:04 PMwithTimeout
and channel functions (the one mentioned in the article you linked) throw subclasses of CancellationExceptions
in situations where the caller of the function is not cancelledJoffrey
03/20/2023, 4:05 PMrunCatching
would actually want to catch thoseJoffrey
03/20/2023, 4:07 PMRoman actually thinks this article is on point 😉 (from what I could tell)Btw, I don't want to put words in your mouth @elizarov so please correct me if I'm wrong in saying this
OG
03/21/2023, 2:53 PMOMG this code is horrendous to read to be frank. I would love to see arguments in favor of this. It's also wrong because in suspending contexts this will catchFWIW - I have used these wrappers for properly handling CancellationExceptions..., which would be incorrect 99% of the time (edited)CancellationException
/**
* Executes [block] and returns a [Result]. All exceptions thrown are
* returned as a [Result.Failure] except a [kotlinx.coroutines.CancellationException]
* which will be rethrown.
*
* See [Throwable.isCancellation] for more info.
*/
inline fun <T> runCatchingIgnoreCancelled(block: () -> T): Result<T> = runCatching(block)
.onFailure { error ->
if (error.isCancellation()) {
throw error
}
}
/**
* Extension on a [Throwable] that returns true if it is of type [CancellationException]. Typically,
* this type of Exception should _not_ be caught and should be bubbled up. Coroutines internally
* uses [CancellationException] for cancelling of work, and they are ignored by all handlers by default.
*
* See [cancellation-and-exceptions](<https://kotlinlang.org/docs/exception-handling.html#cancellation-and-exceptions>)
*/
fun Throwable.isCancellation() = this is CancellationException && this !is TimeoutCancellationException
This was mainly useful in UseCase/domain layer, where the consumer/presentation layer didn't care about the lower level error or why something failed, but only if there was a result or not....Joffrey
03/21/2023, 3:18 PMTimeoutCancellationException
. Imagine your use case layer `await`s on an Deferred
produced by some async
in a different coroutine scope. If that other coroutine scope is cancelled (NOT the one of your use case doing the await()
), the await
will throw CancellationException
- which is just another error in this case, representing the absence of value. In this case you will re-throw and it will not go through your classic error handling that you intended to hook via runCatching
. Same goes for channels `send`/`receive`. Cancelling some other coroutine at the other end of the communication channel shouldn't really be considered like other cancellations by your code, but rather like an error (most of the time). See https://betterprogramming.pub/the-silent-killer-thats-crashing-your-coroutines-9171d1e8f79b?gi=b4555a1271e9 for more details about these "weird" `CancellationException`s.Joffrey
03/21/2023, 3:20 PMThis was mainly useful in UseCase/domain layer, where the consumer/presentation layer didn't care about the lower level error or why something failed, but only if there was a result or notSo you mean you decided on purpose that you don't want to have log reports of
ClassNotFoundException
, ThreadDeath
, NoSuchMethodError
, StackOverflowError
, ArithmeticException
, IllegalArgumentException
etc. but you just want to swallow those errors and show an empty screen?
What do you do in the upper layer with the failure case of those results?OG
03/21/2023, 3:29 PMfold
or more logic in your onFailure
hook. It's up to you... I am simplifying some stuff here just to not derail the convo from what (I think?) was mainly the danger of not bubbling up CancellationException
properly...
But quick/short answer for you, yes, in these cases, the UI required all data to be present, or none, and in the case of any error, there was specific UI shown for retrying, continuing and trying later, or contact info for more help etc... hope that is a bit more clear 🙂Thad
03/23/2023, 2:24 PMrunCatching
doesn’t do anything that try/catch doesn’t do except provide some syntactic sugar to make things like exception recovery and chaining events easier.Thad
03/23/2023, 2:28 PMCLOVIS
03/23/2023, 2:32 PMtry-catch
is bad if you write catch (e: Throwable)
. runCatching
is always bad.CLOVIS
03/23/2023, 2:33 PMrunCatching
would be fine.Thad
03/23/2023, 2:33 PMThad
03/23/2023, 2:33 PMJoffrey
03/23/2023, 2:34 PMI am simplifying some stuff here just to not derail the convo from what (I think?) was mainly the danger of not bubbling up CancellationException properlyActually the point about cancellation exception is the side track. The main point of the question is whether using
runCatching
everywhere is a sane idea.CLOVIS
03/23/2023, 2:34 PMThrowable
. In my opinion it is not a sufficient use case to add a standard library method that breaks coroutines without saying anything in its documentation, tempting people to use it everywhere (and this thread has shown that people do, in fact, use it everywhere).Joffrey
03/23/2023, 2:35 PM“hey I don’t think I’ll have an exception here, but just in case I want to make sure it doesn’t crash the app and gets logged”Could you please elaborate on what you intend to do in the
catch
(or onFailure
) in this case? Crashing the app does get logged, trying to recover from any Throwable
is wrongCLOVIS
03/23/2023, 2:36 PMrunCatching
. If you have a legitimate use case to catch Throwable, then do so explicitly.Thad
03/23/2023, 2:36 PMJoffrey
03/23/2023, 2:37 PMrunCatching doesn’t do anything that try/catch doesn’t do except provide some syntactic sugar to make things like exception recovery and chaining events easier.It hides a catch of
Throwable
which is almost never a good idea to catch, unless you're writing a framework that runs user-provided code.
It also adds boilerplate on the access of the successful value in the happy path.OG
03/23/2023, 2:37 PMActually the point about cancellation exception is the side track. The main point of the question is whether using runCatching everywhere is a sane idea.
Gotcha, I guess my 2cents here is a definite no, it's not a rule/style/convention that should be used entirely in a coroutine based codebase. Only in places where it makes sense. (Like the above examples just shared potentially)
Thad
03/23/2023, 2:38 PMCould you please elaborate on what you intend to do in theI intend to show an error and log it instead of having the user force exit the app, it’s a pretty crappy user experience the other way(orcatch
) in this case? Crashing the app does get logged, trying to recover from anyonFailure
is wrongThrowable
CLOVIS
03/23/2023, 2:39 PMCLOVIS
03/23/2023, 2:40 PMJoffrey
03/23/2023, 2:40 PMI intend to show an error and log it instead of having the user force exit the app, it’s a pretty crappy user experience the other wayThen this should be at the top of the hierarchy, not deep in the business code
Nino
03/23/2023, 2:40 PMJoffrey
03/23/2023, 2:40 PMThad
03/23/2023, 2:41 PMThrowable
preferably but instead Exception
that’s fairCLOVIS
03/23/2023, 2:42 PMException
.CLOVIS
03/23/2023, 2:42 PMJoffrey
03/23/2023, 2:42 PMException
you don't have the hard JVM issues, but still it's not advised to "catch everything". But in any case runCatching
catches Throwable
, so it settles the question about using runCatching
in many places in business codeCLOVIS
03/23/2023, 2:43 PMIllegalArgumentException
. If you ever get one, it means your code is wrong. There's no recovering from that. Also, CancellationException
is an Exception
, so you're still breaking coroutines.Thad
03/23/2023, 2:43 PMJoffrey
03/23/2023, 2:44 PMCLOVIS
03/23/2023, 2:44 PMmain
method or immediately after that). Everywhere else in your codebase, you should not catch stuff you're not aware of.Thad
03/23/2023, 2:44 PMrunCatchingIgnoreCanceled
trick @OG posted earlier. But it’s up to the consumer to do the right things with the errors they are catchingThad
03/23/2023, 2:47 PM!!
is a kotlin feature that really should never (or rarely) be used but I don’t think I’ve ever heard someone argue it shouldn’t exist just because they don’t think it’s used often. If you don’t like it you don’t have to use it. I’ll continue using it safely (though I might make a new version of it to cover the Throwable case you accurately pointed out, I forgot it caught more than exception)Nino
03/23/2023, 2:48 PM!!
helps migrating code from Java 😄Joffrey
03/23/2023, 2:49 PM!! is a kotlin feature that really should never (or rarely) be used but I don’t think I’ve ever heard someone argue it shouldn’t exist just because they don’t think it’s used oftenI think this was added for pragmatism, but I also believe no reasonable application should use it. It's always superior to use
?: error("reason why you believe it's never null")
. Except in code golf.CLOVIS
03/23/2023, 2:49 PM!!
is an explicit mark for the reviewer that they should pay attention to that piece of the code. Most of use are taught in school to be very careful around catch (e: Throwable)
, for good reason. Most of us don't realize how dangerous runCatching
is, which is its main issue.Joffrey
03/23/2023, 2:50 PMOk I still prefer to not crash the app when I could just post an errorWe don't disagree on this. What we're saying is that you should prevent the app from crashing in a central place, show the error popup, and then still exit. For this, you don't need
runCatching
nor catch(Throwable)
anywhere in the business code, just in one place close to the entrypoint of your program.OG
03/23/2023, 2:54 PMOG
03/23/2023, 2:55 PMNino
03/23/2023, 2:56 PMException
OG
03/23/2023, 2:57 PMJoffrey
03/23/2023, 2:58 PMCancellationException
be in general. If I'm catching specific exceptions, 99.9% of the time this doesn't include CancellationException
, so it's fine to not care about it. If you do write a catch-all block somewhere, it's usually high-enough in the hierarchy and everything will be rethrown anyway in the catch
.CLOVIS
03/23/2023, 2:59 PMensureActive
.CLOVIS
03/23/2023, 3:02 PMtry {
withTimeout(5000) {
someNetworkRequest().toList()
}
} catch (e: TimeoutCancellationException) { // I'm catching a subclass, so if I'm cancelled it will bubble up without creating zombies
// Here, I know that the timeout failed and it's safe to recover however I want
return emptyList()
}
OG
03/23/2023, 3:03 PMJoffrey
03/23/2023, 3:03 PMwithTimeout
I would just use withTimeoutOrNull
and check for null. It is simply safer because you can be sure that it's your own timeout in that case. Otherwise you can have problems with nested withTimeout
Nino
03/23/2023, 3:04 PMwithTimeoutOrNull(5000) {
someNetworkRequest().toList()
} ?: emptyList()
CLOVIS
03/23/2023, 3:04 PMtry {}
catch (e: CancellationException) {}
This is fine, but less readable (and a bit slower) than catching TimeoutCancellationException
try {}
catch (e: CancellationException) {
ensureActive()
// your stuff
}
Nino
03/23/2023, 3:04 PMnull
was a nightmare in Java, null
a strength in KotlinCLOVIS
03/23/2023, 3:06 PMwithTimeout
, where it would be safe to catch CancellationException
, there's a nicer way to do things 😅 So just always find a way never to touch CancellationException
, everything will be easier this wayOG
03/23/2023, 3:08 PMOG
03/23/2023, 3:08 PMJoffrey
03/23/2023, 3:13 PMrunCatching
is equivalent to try/catch with Throwable // dangerous!
• Better to use `try`/`catch` and handle specific errors you care about
• Typically you don't need to handle catching CancellationException
if you follow the previous point
• An exception to the previous point is catching TimeoutCancellationException
when using withTimeout
, but in that case using withTimeoutOrNull
is a better option
And I would add that catching Exception
(or even Throwable
) can be useful in a very top-level place in order to avoid crashes and allow to terminate the application more gracefully. Catching Exception
can also be useful if you want to rethrow a more high level exception with the original one as cause.OG
03/23/2023, 3:16 PMayodele
03/24/2023, 8:08 AMdata
or domain
layer should not contain any error catching, let them throw, and you handle them in the top most level e.g the ViewModel
?CLOVIS
03/24/2023, 8:11 AMCLOVIS
03/24/2023, 8:12 AMCLOVIS
03/24/2023, 8:16 AM