question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Circular exception with async/await in coroutineScope

See original GitHub issue

A circular JobCancellationException from coroutines keeps causing a StackOverflowException in logback which I use for logging. IMO the recursion should be broken up at some point.

This is quite similar to #305.

Output:

java.lang.RuntimeException: 1
   suppressed: java.lang.RuntimeException: 2
      cause: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=ScopeCoroutine{Cancelled}@257ed010
         cause: java.lang.RuntimeException: 1 // RECURSION!

Repro:

suspend fun main() {
   try {
      coroutineScope {
         val def1: Deferred<Unit> = async {
            delay(100)
            throw RuntimeException("1")
         }
         val def2: Deferred<Unit> = async {
            try {
               while (true)
                  delay(10)
            }
            catch (e: Exception) {
               throw RuntimeException("2", e)
            }
         }

         def1.await()
         def2.await()
      }
   }
   catch (throwable: Throwable) {
      debug(throwable)
   }
}


fun debug(throwable: Throwable, prefix: String = "", processed: MutableSet<Throwable> = hashSetOf()) {
   print(throwable)

   if (processed.contains(throwable)) {
      println(" // RECURSION!")
      return
   }
   println()

   processed += throwable

   throwable.cause?.let {
      print("$prefix   cause: ")
      debug(it, prefix = "$prefix   ", processed = processed)
   }
   throwable.suppressed?.firstOrNull()?.let {
      print("$prefix   suppressed: ")
      debug(it, prefix = "$prefix   ", processed = processed)
   }
}

Workaround is to catch CancellationException and rethrow it without wrapping it into another exception.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:8
  • Comments:12 (5 by maintainers)

github_iconTop GitHub Comments

4reactions
qwwdfsadcommented, Jun 25, 2019

I understand your pain with circular exceptions and I would like to fix this problem as well.

What about changing the CE’s cause dynamically depending on the coroutine context? According to Throwable.getCause() documentation it is fine to override the method.

It is indeed okay to override getCause, but making it non-pure is not: it will confuse both developers and some frameworks (e.g. I am not sure that all logging frameworks handle exceptions with non-pure getCause without bugs).

What about changing the CE’s cause dynamically depending on the coroutine context?

It is another layer of complications because coroutine may not have a context (e.g. just a Job() created without a coroutine). Additional complexity comes from the fact that some coroutine handle their own exceptions, while others don’t etc.

What I think we can do is to use CE without a cause when coroutine has a parent, though I am not sure this is a valid change. I will investigate it further

2reactions
fluidsoniccommented, Jun 19, 2019

Thank you for the detailed explanation. I understand the cause and the reasoning. Yet it ends up in a cycle and cycles don’t make sense for exceptions.

Unfortunately that puts our case in kind of a deadlock between this issue with Kotlin coroutines and logback which doesn’t support circular exceptions although the issue is open for 5 years now - resulting in a StackOverflowError. Bottom line is that we lose some stability in the back-end, which is not good. These issues are difficult to foresee while programming because CEs can potentially be thrown all over the place, so there would be a need for a lot of checking for CEs and a lack of context if we cannot wrap it.

What about changing the CE’s cause dynamically depending on the coroutine context?

  • If the context is the same as where the CE was thrown then cause returns the cause (RE(1)) because otherwise that information would be inaccessible.
  • If the context is different then we’re either in a parent or in a completely different one in which case cause then returns null. In the parent the cause RE(1) is still accessible because it’s the root exception and we merely have broken the cycle.
  • There could be an additional property just for CEs which always returns the cause independent of the current context. That would allow for some edge cases where the developer still needs to access the cause in a context which is neither the cancelled coroutine nor a parent.

According to Throwable.getCause() documentation it is fine to override the method.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Kotlin: How to wait for a coroutine from non-suspend without ...
So to call a suspend function from a non-suspend function, without using runBlocking, you have to create a coroutine scope.
Read more >
Exceptions in coroutines. Cancellation and Exceptions in…
Imagine a UI-related CoroutineScope that processes user interactions. If a child coroutine throws an exception, the UI scope will be cancelled and the...
Read more >
Async/Await Error Handling - Beginner JavaScript - Wes Bos
We will talk about error handling strategies for async await in this lesson. Because there is no .then() that we are chaining on...
Read more >
Async Operations with Kotlin Coroutines — Part 1
Suspending functions; Coroutine context and Coroutine Scope ... coroutine with a return value or with an exception if an error had occurred ...
Read more >
Python behind the scenes #12: how async/await works in Python
Python's implementation of async / await adds even more concepts to this ... If the generator returns something, the exception holds the ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found