https://kotlinlang.org logo
#coroutines
Title
# coroutines
t

Timo Drick

03/08/2021, 4:54 PM
It looks like try catch around a background task will prevent interruption of a coroutine:
Copy code
val job1 = launch {
    try {
        withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
            SystemClock.sleep(1000)
            throw IOException("Wurst")
        }
    } catch (err: Throwable) {
        Log.d("Test", "code executed")
    }
}
launch {
    delay(500)
    job1.cancel()
}
In this code the Log.d line will be executed. Which is bad when e.g.: i want to show a error message after a network call failed but the fragment coroutine scope is already cancelled. Is there any ellegant solution to avoid executing the catch when corotuine scope is already cancelled?
t

Tijl

03/08/2021, 5:05 PM
ensureActive()
before you throw, but if you would use
delay()
instead of sleep that would also just work. however your catch is catching the
CancellationException
so your message will always display, you need to narrow your catch or check it’s not
CancellationException
e

ephemient

03/08/2021, 5:15 PM
in general : don't catch Throwable/Exception, and if you do, definitely don't just swallow all throwables/exceptions. it should work if you catch IOException here
there's other exceptions that are also bad to swallow, e. g. InterruptedException
t

Timo Drick

03/08/2021, 9:17 PM
My code is just simplified real world application. So in real i do network calls which could throw execeptions. An i do want to catch this exceptions. And i am using coroutines to avoid the callback hell.
c

Chandler Cheng

03/08/2021, 9:18 PM
try launch(exceptionHandler) { … } ?
e

ephemient

03/08/2021, 9:37 PM
@Timo Drick ok, but even if you do want to catch Exception, you have to re-throw exceptions you don't know how to handle. this includes Kotlin's CancellationException but also a variety of other possible Java exceptions as well
t

Timo Drick

03/08/2021, 9:40 PM
Maybe but in my UI code than i just want to show a general error message.
Maybe i should change my suspend network functions to not throw any exceptions. But Kotlin do not support union data types. This means for every call i have to introduce a interface which contains the expected data from the network call and a error data type. Which introduces boilerplate code.
t

Tijl

03/08/2021, 9:49 PM
there are no restrictions on throwing exceptions, just catch
CancellationException
separately.
t

Timo Drick

03/08/2021, 9:50 PM
My problem is that i assumed that when i execute code in the first coroutine scope that i can be sure that this scope is not cancelled. But when i try catch s.th. this is not true any more.
t

Tijl

03/08/2021, 9:54 PM
ah, no. Indeed cancellation is cooperative, not magic. but it will stop at any suspend point, or as I suggested use
ensureActive()
throwing exception or not throwing exceptions won’t magically solve whatever control flow problem you have.
t

Timo Drick

03/08/2021, 9:56 PM
Copy code
val job1 = launch {
    //try {
        withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
            SystemClock.sleep(1000)
            //throw IOException("Wurst")
        }
    //} catch (err: Throwable) {
        Log.d("Test", "this code is not executed")
    //}
}
launch {
    delay(500)
    job1.cancel()
}
This code shows that the happy path do work. So when i do a long running background operation and the coroutine is already cancelled nothing is executed any more. But in a try catch exception case it is not checked.
It looks like throwing exceptions is not interruptable.
This code also works:
Copy code
val job1 = launch {
    try {
        withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
            SystemClock.sleep(1000)
            throw IOException("Wurst")
        }
    } catch (err: Throwable) {
        delay(1)
        Log.d("Test", "this code is not executed")
    }
}
launch {
    delay(500)
    job1.cancel()
}
I just want to mention that for me this is not an expected behavior of coroutines.
And i have to change a lot of code to avoid this kind of situations. And the goal of the coroutine scopes was to avoid checking every time that the UI is still active.
t

Tijl

03/08/2021, 10:26 PM
the end of withContext is a suspension point, so the coroutine execution will stop there (by throwing a
CancellationException
) if you canceled it, naturally. Indeed throwing exceptions is not a suspension point (I guess this is what you mean by “interruptable”) I think you’re seeing coroutines as a kind of threads that stop executing in the middle of what they’re doing, but that would be terrible. Instead they’re cooperative, their behaviour is predictable, you know your code will run as normal until you come to a defined point (a method marked with
suspend
) Just think about it.. you have a piece of code that throws the exception, but sometimes you want the execution will stop right before that statement (intrupted as you say), and sometimes not (e.g. the cancel is called a little later). You will have to account and test for both states (with and without exception thrown), though the code that would have caused the condition you wanted to raise the exception for in both cases.
delay is a suspend method too, which is why your last example works. but they’re all clearly marked methods, providing predictable points at which your execution can stop.
t

Timo Drick

03/08/2021, 10:30 PM
That is very bad 😞 This means i have to remember to check every time in a catch block if the coroutine is not cancelled
Is there maybe some kind of cooperative exception throwing? The problem is that the background corotuine do not know if the calling coroutine in the UI thread is still running.
Or cooperative exception catching
i only want to catch the exception when my scope is not cancelled. No i want to catch anyways but i only want to execute the code inside of the catch when it is not cancelled
t

Tijl

03/08/2021, 10:34 PM
your case is unusual (you throw an exception, yet you don’t want to do anything with it) but that does not mean you can’t implement it, just put
ensureActive()
as the first statement in your catch
or put the code that handles your exception in a
suspend
method
Copy code
} catch (err: Throwable) {
    handleError(err)
}

// ...
suspend fun handleError(err:Throwable) {}
t

Timo Drick

03/08/2021, 10:36 PM
My code is not unusual. That is code which is needed all over the place for Android UI / background work interaction.
I am just searching for an ellegant way of doing things without so many side effects. Of course i know how to fix this case. But i want a general solution.
t

Tijl

03/08/2021, 10:38 PM
I think you and I have different opinions on what to do do with exceptions, but that’s ok. the two examples I gave are both general solutions.
t

Timo Drick

03/08/2021, 10:40 PM
Ok here some pseudo code of a general ui problem:
Copy code
launch(Dispatchers.Main) {
    showLoadingSpinner()
    try {
        val data = downloadData()
        showData(data)
    } catch (err: Throwable) {
        showError()
    } finally {
        hideLoadingSpinner()
    }
}
e

ephemient

03/08/2021, 10:41 PM
catch (err: Throwable)
like that is a bad idea, coroutines or not.
t

Timo Drick

03/08/2021, 10:42 PM
So there is no way of doing this correct without messing the code up?
t

Tijl

03/08/2021, 10:43 PM
but if you want every catch to act as a suspension point, you’ll have to write a compiler plugin (not as hard as it sounds), that’s just not in the language because most programmers won’t want every catch to act as a potential branch point. or just use one of the two very simple solutions, e.g in your example if
showError
is marked
suspend
you’re already done.
both suggestion I made are “correct” and neither “mess up the code”
t

Timo Drick

03/08/2021, 10:45 PM
Ok yes. Thank you very much. The problem is that in practice it is hard to keep this in mind. But you are right. Thanks @Tijl and @ephemient for you explanations.
Maybe better solution which i also prefer when working with Compose is to put the error inside of the returned data instead of throwing exeptions.
This also forces the user of an API to handle errors.
e

ephemient

03/08/2021, 10:48 PM
+1 to that, Compose or not
t

Timo Drick

03/08/2021, 10:48 PM
Copy code
launch(Dispatchers.Main) {
    showLoadingSpinner()
    val data = downloadData()
    hideLoadingSpinner()
    when(data) {
        is Data -> showData(data)
        is Error -> showError(data)
    }
}
t

Tijl

03/08/2021, 10:49 PM
if you have patterns that are a little unusual and you want to enforce them (E.g ensureActive after every catch), you can make a linter rule, annotation processor, compiler plugin etc. I’ve sufficiently explained why to give catch that behaviour by default would be too problematic. Indeed a better solution is to write less procedural code, and move more towards a state based model (as Compose also pushes you to do)
t

Timo Drick

03/08/2021, 10:50 PM
Yes thanks. I want to keep every thing as simple as possible. So writing linter rules and compiler plugins is not what i want 😄
e

ephemient

03/08/2021, 10:51 PM
my team has gotten a lot of mileage out of both Lint rules and bytecode transformations. no compiler plugins needed (yet...)
t

Timo Drick

03/08/2021, 10:51 PM
Unfortunatley this means for my current project that i have to change a lot of code.
Working with coroutines for years now and still learning every day new things 😄
t

Tijl

03/08/2021, 10:58 PM
Yes much to learn always. But understanding that coroutines don’t magically stop executing code after every statement (or in the middle) when canceling is a very good one to learn. Now it seems problematic (for your specific use case), but if you don’t properly understand this you must also be worried a lot about “what if my code stops right after this statement”.
t

Timo Drick

03/08/2021, 11:01 PM
I know this things. I just didn't know that catching an exception is not cooperative.
Or throwing the exception is not interruptable how every you would call this.
t

Tijl

03/09/2021, 7:25 AM
That’s still based on a too magical view of
suspend
. It doesn’t alter any other language mechanics (like throw), it’s essentially just a shortcut for throwing some blocks around your code, capturing some variables, and passing it to a dispatcher. it’s something you can (and probably have) at some point basically implemented yourself in Java or some other language.
t

Timo Drick

03/09/2021, 8:37 AM
In this code the exception is thrown. But delay is cooperative so it should be cancelled?
Copy code
vb.button.setOnClickListener {
    val job1 = launch {
        try {
            withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
                SystemClock.sleep(1000)
                delay(10)
                throw IOException("Wurst")
            }
        } catch (err: Throwable) {
            Log.d("Test", "code executed")
        }
    }
    launch {
        delay(100)
        job1.cancelChildren()
        job1.cancel()
    }
}
t

Tijl

03/09/2021, 8:40 AM
yes delay is marked
suspend
but again,
ensureActive()
will give the exact same result for cancellation, a
CancellationException
will be thrown. Because, again, not much “magic” about coroutines, they don’t stop execution of a codepath somehow (which would require changing all kinds of language rules and implementation), they just throw this exception.
and you’re still catching it, so it will go there
e.g. consider
Copy code
} catch (err: Throwable) {
            Log.d("Test", "code executed")
        }
to
Copy code
} catch(e: CancellationException) { throw e }
catch (err: Throwable) {
            Log.d("Test", "code executed")
        }
t

Timo Drick

03/09/2021, 8:51 AM
ok i see. A littlebit wired for me this also executes the throw:
Copy code
withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
    SystemClock.sleep(1000)
    if (isActive) {
        throw IOException("Wurst")
    }
}
Ok i think i understand now. So because of the cancellation an exception is thrown.
t

Tijl

03/09/2021, 8:53 AM
why would it not? you’re telling it to throw… isActive is not a suspend function (it’s not even a function) (pretty sure it does not throw that ioexception, but cancellation)
t

Timo Drick

03/09/2021, 8:58 AM
Ok yes thanks @Tijl for you patience. But coroutines are not trivial 😄
t

Tijl

03/09/2021, 9:01 AM
no they are not, but they are in fact much more simple than they seem, I hope this illustrated that. a suspend method checking a cancel flag and then throwing an exception is way more simple to understand than a code block that magically stops executing. But indeed a lot of the introductions to coroutines skip over that part.
t

Timo Drick

03/09/2021, 9:08 AM
Also in practice it turnes out that debugging is also more complicated when threads are changed because than you do not see who called the coroutine in the backstack
t

Tijl

03/09/2021, 9:12 AM
IntelliJ 2020.3 has a coroutine debugger that will show the callstack across threads: https://www.jetbrains.com/help/idea/debug-kotlin-coroutines.html?gclid=Cj0KCQiA1pyCBh[…]5ykAxBkZ1zZheIIF0n4CKO0QDItGEsw7-hOVyXwgrrClhXIaArdCEALw_wcB this is also in AS ArticFox
🔥 1
t

Timo Drick

03/09/2021, 9:13 AM
i was hoping that some thing like that will be possible in the futur 😄
t

Tijl

03/09/2021, 9:16 AM
it still needs more work, not even sure it works for Android well at the moment
4 Views