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 suspend keyword is added to prevent this new function from being called in regular non-suspending/blocking code.
  • When code calls the StdLib’s runCatching in a Coroutine/suspendable-context, a lint warning should warn the developer that handling cancellations may be compromised and runSuspendCatching should be used instead.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 94
  • Comments: 71 (25 by maintainers)

Most upvoted comments

runCatching is 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 function Result<*>.except<reified T : Throwable>() or Result<*>.throwIfIsInstance<reified T : Throwable>() that would rethrow. For example:

inline fun <reified T : Throwable> Result<*>.except(): Result<*> =
    onFailure { if (it is T) throw it }

It would allow you to:

runCatching { mightThrowCancellationExceptionThatShouldNotBeCaught() }
    .except<CancellationException>()
    .onFailure { throwable /* Will never be of type CancellationException */ -> }

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 CancellationException at 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.

As noted in #1814 (comment) Flow.catch {} does re-throw CancellationException.

Indeed. I understand that Flow is in the coroutine world and runCatching is 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 the runCatching wraps a suspendable piece of code (i.e. it could throw a CancellationException), the runCatching should re-throw any CancellationException. If not the suspendable caller of runCatching may 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 runSuspendCatching can fix the cancellation-handling issue caused by runCatching:

fun main() {
    val screenScope = CoroutineScope(Dispatchers.IO)

    println("Start")

    screenScope.launch {
        println("Launched")

        val result = runCatching {
            delay(1000)
            4
        }.getOrElse { 0 }

        writeToDatabase(result)
    }

    Thread.sleep(500)

    // User leaves the screen.
    screenScope.cancel()

    println("Job was cancelled: ${screenScope.coroutineContext[Job]?.isCancelled}")
    println("Done")
}

suspend fun writeToDatabase(result: Int) {
    println("Writing $result to database")
}

Running the above main function prints out:

Start
Launched
Writing 0 to database
Job was cancelled: true
Done

This is not right. The screenScope was cancelled, the call to writeToDatabase should not have happened. But it did happen, because the runCatching did not re-throw the CancellationException thrown by the delay(1000) statement.

If we change the code like this (replacing runCatching with runSuspendCatching):

fun main() {
    val screenScope = CoroutineScope(Dispatchers.IO)

    println("Start")

    screenScope.launch {
        println("Launched")

        val result = runSuspendCatching {
            delay(1000)
            4
        }.getOrElse { 0 }

        writeToDatabase(result)
    }

    Thread.sleep(500)

    // User leaves the screen.
    screenScope.cancel()

    println("Job was cancelled: ${screenScope.coroutineContext[Job]?.isCancelled}")
    println("Done")
}

suspend fun writeToDatabase(result: Int) {
    println("Writing $result to database")
}

this is printed out:

Start
Launched
Job was cancelled: true
Done

This is correct. The call to writeToDatabase was not made at all, which is the desired behavior (try replacing runSuspendCatching with just a plain run and remove the getOrElse; you’ll get the same result).

Bad news: there’s no version of runCatching that 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 CancellationException means that the current coroutine was cancelled: an operation (for example, Deferred.await) could throw CancellationException just 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 the CancellationException came was just rethrown from some other coroutine. The recommended pattern if you absolutely must catch every exception (and you typically don’t!) is:

try {
  operation()
} catch (e: Throwable) {
  if (e is CancellationException) ensureActive() // will throw a `CancellationException` if the current coroutine was cancelled
  processException(e) // process (for example, log) the exception from `operation`
}

It’s also not correct to assume that a CancellationException is safe to ignore if the current coroutine was not cancelled. For example, Flow can throw CancellationException from emit to 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:

try {
  emit(value)
} catch (e: Throwable) {
  if (e is CancellationException) throw e
  processException(e)
}

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:

public suspend inline fun <R> runSuspendCatching(block: () -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (t: TimeoutCancellationException) {
        Result.failure(t)
    } catch (c: CancellationException) {
        throw c
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

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 {} and Flow.catch {} make it easy to use exception for control flow which can be dangerous as showcased here.

PS: also, the fact that Result was only available from runCatching {} for some time was an incentive to use the dangerous runCatching {}

A newbie shouldn’t catch all exceptions, that’s the bad pattern to avoid, and easy to spot. Coroutines CancellationException is 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 runCatching and onFailure

Drawbacks:

  • I don’t really want to catch a Throwable
  • I have to re-throw the CancellationException
  • I have to repeat the re-throwing logic everywhere I use it

2. Using the language try-catch

Drawbacks:

  • I have to re-throw the CancellationException
  • I have to repeat the re-throwing logic everywhere I use it

3. Using the flow API

Drawbacks:

  • I don’t really want to catch a Throwable
  • I have to know how to use the flow API
  • Seems more verbose (flow, catch, emit, single pattern)

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 Throwable and 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 language try-catch but they don’t really bother re-throwing the CancellationException (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 runCatching in an application code? Can you give a code snippet that motivated you to come with this proposal?

FWIW, the catch operator of Flow already specifically handles CancellationException in that it will re-throw it if the Throwable instance is of type CancellationException.

I also asked about a similar runCatching variant for suspend functions in https://medium.com/@mhernand40/thank-you-for-the-reply-dd01cf846ab7.

Off-topic. Shouldn’t there be a warning in runCatching documentation? 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 know runCatching is not a part of coroutines API) will prevent erroneous and unconscious usage of this function?

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?

After Kotlin 1.7 underscore operator for type arguments, @erikhuizinga solution is even more appealing because you can use it that way:

inline fun <reified T : Throwable, R> Result<R>.except(): Result<R> = onFailure { if (it is T) throw it }
// ...
.except<CancellationException, _>()

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 Throwable in 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).

A newbie shouldn’t catch all exceptions, that’s the bad pattern to avoid, and easy to spot. Coroutines CancellationException is just one example of an exception you shouldn’t have tried to catch.

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 without catch (e: Exception).

Any update on this ?

Unfortunately, no. There are two conceptual problems we see here:

  1. The name should be concise enough to be usable yet clearly reflect the intent. We do not have such a name yet
  2. We are not sure that it is completely safe to catch and ignore arbitrary cancellation exception, yet we do not have a mechanism to distinguish e.g. Java-specific CE from coroutines one.

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 Result in a declarative manner, then except<> extension might do the trick. For others, runSuspendCatching might be preferable and it’s crucial for us to understand what exactly is being solved.

Why not instruct the compiler to automatically re-throw the CancellationException in a catch block surrounding a suspending function ? … at the compilation level when generating the code behind coroutines.

The compiler knows nothing about kotlinx coroutines, neither it knows about CancellationException and 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 runCathing function should rethrow any InterruptedException?

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 suspendedRunCatching in our whole codebase that uses coroutines.

@SSong-develop can I rewrite your example?

fun main() {
    val screenScope = CoroutineScope(Dispatchers.IO)
    val anotherScope = CoroutineScope(Dispatchers.Default)

    println("Start")

    val anotherTask = anotherScope.async { delay(1000); return@async 4 }

    screenScope.launch {
        println("Launched")

        val result = runSuspendCatching { anotherTask.await() }.getOrElse { 0 }

        writeToDatabase(result)
    }

    Thread.sleep(500)

    // User leaves the screen.
    anotherScope.cancel()

    println("Job was cancelled: ${screenScope.coroutineContext[Job]?.isCancelled}")
    println("Done")
}

What should return runSuspendCatching?

This issue includes the same problem of #3658. Catching a CancellationException does not implies that the current job has been cancelled. So a runCatching should include an ensureActive, because a CancellationException can imply the cancellation of the current job. However, the runCatching can produce a Result even if the current job has been cancelled, so runCatching should execute runCatching in any case? In other words: does runCatching check 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 in runCatching. In other words: running in a cancelled scope runCatching{ delay(0) } should return a success or a failure?

In #3658, in after each catch(Throwable) should be executed an ensureActive(), similarly after each runCatching should be executed an ensureActive().

Any update on this ?

Being able a catch a CancellationException in 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 a finally and / or with NonCancellable, 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, any Error thrown 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 null in case of ClassNotFoundException? 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 the httpClient.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 the CancellationException problem.

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 caught CancellationException further 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 of runCatching that would clearly document these details.

Any recommendations of how they could improve or is this a valid usecase?

They use this suspendRunCatching private function only once in the file, and only check isSuccess on the returned result. They could technically just catch and return false in case of failure. There is no need for a Result type here.

Also, they catch Exception, which is better than catching Throwable, but then it’s not really similar to runCatching anymore, 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 runCatching that ignores CancalletionException instead of runCatching that catches all, but invokes ensureActive right 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.

But why can’t we easily wrap exception based APIs (bad) to null / type-safe (good) ?

null should 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, returning null instead 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 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?

After Kotlin 1.7 underscore operator for type arguments, @erikhuizinga solution is even more appealing because you can use it that way:

inline fun <reified T : Throwable, R> Result<R>.except(): Result<R> = onFailure { if (it is T) throw it }
// ...
.except<CancellationException, _>()

the type of the item inside the result will be automatically inferred by the underscore, without the need to use *.

It is possible to omit the underscore if such function is introduced as a member function of Result:

Index: libraries/stdlib/src/kotlin/util/Result.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/libraries/stdlib/src/kotlin/util/Result.kt b/libraries/stdlib/src/kotlin/util/Result.kt
--- a/libraries/stdlib/src/kotlin/util/Result.kt	(revision 8798fdeb821bcc3780138775b315accb06224662)
+++ b/libraries/stdlib/src/kotlin/util/Result.kt	(date 1697031318606)
@@ -53,6 +53,9 @@
             else -> value as T
         }
 
+    @InlineOnly
+    inline fun <reified E : Throwable> rethrow(): Result<T> = onFailure { e -> if (e is E) throw e }
+
     /**
      * Returns the encapsulated [Throwable] exception if this instance represents [failure][isFailure] or `null`
      * if it is [success][isSuccess].

If I’m not mistaken, it won’t become the actual member in the bytecode due to @InlineOnly.

suspend fun faultyCalculation(): List<Int> = TODO()
suspend fun usage() {
    val result = 
        runCatching { faultyCalculation() }
            .rethrow<CancellationException>()
}

It also would be nice to see more real-world use cases for such API to understand what kind of problems are being solved.

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 plain try / 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 CancellationException I’ll want to rethrow it or just ignore it. It would be nice to just have some runCatching function 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 handling CancellationException from the library. Maybe you can include such a thing and also link to this utility function?

You completely missunderstand my points almost like a strawman

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’m only talking about environment-induced exceptions, not logical (programming) errors or even Errors. Obviously even in my code I’ll require() 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.

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 null for everything doesn’t give information about anything, just like using Any? 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).

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 ?

Catch the IOException either on the spot or at a level that makes sense for this call? I mean, I’m only saying not to catch a general Exception at 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 in catch(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 require catch(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 catch Exception in general in these extensions, those are no longer business code, and I’m almost fine with those. But you’re talking about putting catch(Exception) throughout your code, which is different.

It feels not Kotlin at all in its call APIs. Why can’t I have a simple getOrNull() / postOrNull() / etc… like we have Int.parseOrNull(), Duration.parseOrNull(), List.getOrNull() and so like we do in the rest of the Kotlin ecosystem

(NB: I’m assuming you mean String.toIntOrNull when writing Int.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, and Duration.parseOrNull only catches a specific exception too. Both of those could definitely throw exceptions in case of other issues. They don’t contain a catch(Exception) guard or anything, and that’s not the point.

Also, toIntOrNull() is not more idiomatic Kotlin than toInt() (which seemed to be your point). It’s all about expressing a different intention: toInt() means you don’t expect an invalid String here, and it would be a programmer’s error to let one arrive at that point, whereas toIntOrNull() explicitly expects invalid input and deals with it by handling the null result. Ktor offers the same choice by providing expectSuccess = true|false. The case for IOException is indeed debatable, but that’s an orthogonal discussion I believe.

I agree with this but the root of Kotlin’s success was Java compatibility, so we have to live with it.

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’ll require() 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 safety
and
2/ fine grained and boilerplate approach to try / catch exception handling + occasionnal crashes because of missing documentation (or bumping a version that introduce a new thrown exception for example…) + no type safety

I, 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.

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”.

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.

Do you have an example exception-based API that you consider bad?

Ktor yes. It feels not Kotlin at all in its call APIs. Why can’t I have a simple getOrNull() / postOrNull() / etc… like we have Int.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:

It always depends on the application

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?

create an excessive guide/docs on exception/error handing in kotlin … prevent miss use and bugs

It would be awesome! Especially if this documentation would contain the known issues and their “workarounds” as well.

There’s nothing to do against Error by definition

So you can’t have 0% crash, that’s my point. Why did you suggest this as a goal at all?

Why should I care if it’s 400 / 500 / parsing ?

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?

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”

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 null for 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.

No need for extra boilerplate to handle an exception I don’t see value in.

Using runCatching + Result management adds boilerplate, exception bubbling is free of syntax (it’s the default). Also, you cannot compare the amount of boilerplate of the runCatching approach with multiple catch blocks handling different errors, because these don’t do the same thing. If you have one generic catch-all (which kinda emulates the runCatching way), 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.

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.

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”.

Why can’t we easily wrap exception based APIs (bad) to null / type-safe (good) ?

Do you have an example exception-based API that you consider bad?

@joffrey-bion There’s nothing to do against Error by 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.

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?

@nbransby Actually I don’t think the use case question was ever about why rethrowing CancellationException. It’s rather about the need for runCatching at 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-throwing CancellationException manually) 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 runCatching marked 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 the Result type 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.

In Android, if I get an OOMError, my process will be terminated anyway, my application will crash and the application may be relaunched afterwards if the Android OS deemed it OK.

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-cache with if (e is CancellationException) simply because we don’t know a good name for the function. runCaching is a bit too aggressive.

My 2 cents about using runCatching and Result in general are expressed in more details here by the way: https://stackoverflow.com/a/70847760/1540818

Great post!