Why async throwing exception without calling await() method?

As per documentation coroutine created by using async builder will throw exception (if its there) after calling await() method on the coroutine. Then why the following code throws exception although await() method has not been called.

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        runBlocking {
            async {
                throw ArithmeticException() // This should not throw an exception until await() is not called
            }
        }
    }

Note: If GlobalScope or any other scope is used with async builder then it does not throw exception but without scope (directly in the runBlocking) it throws exception.

Thank you.

1 Like

Lazily started async:

Optionally, async can be made lazy by setting its start parameter to CoroutineStart.LAZY. In this mode it only starts the coroutine when its result is required by await, or if its Job’s start function is invoked.

runBlocking {
    async(start = CoroutineStart.LAZY) {
        throw ArithmeticException() // This should not throw an exception until await() is not called
    }
}
1 Like

Docs say that it fails the parent scope. You can avoid this by using a SupervisorJob.

The resulting coroutine has a key difference compared with similar primitives in other languages and frameworks: it cancels the parent job (or outer scope) on failure to enforce structured concurrency paradigm.

I don’t know to which place in docs your refer to, but it probably doesn’t say async() throws only after using await(). It says an error in async() is thrown by await(), but as @nickallendev said, it also fails its parent coroutine. If you use GlobalScope your coroutine is detached, so it doesn’t fail anything. Which is actually a disadvantage, because you may very easily miss errors.

1 Like

Maybe I’m getting this wrong, but I think the OP was confused about the fact that async seemingly is meant to be lazy (i.e. it shouldn’t start executing or do anything really until it is awaited). I can see how that behaviour can be surprising, but that expected behaviour is what CoroutineStart.LAZY provides

Thank you for your reply. But if we made launch lazy by setting start parameter to CoroutineStart.LAZY it will also not throw the exception. Then what’s the difference ?

 val job: Job = launch (start = CoroutineStart.LAZY) {
   throw ArithmeticException() // This will not throw the exception until calling job.start() method
        }

Thank you for your reply. Yes, you are right that if a child coroutine fails (throws exception) then the parent will also fail and hence all other children. but here the question is about when the coroutine/builder should throw the exception ? launch builder throws exception when it is occurred and async throws when await() method is called but in the asked question async is throwing exception even without calling await() method.

Honestly, I’m a little confused :slight_smile: The way how it works is that whenever an exception is thrown in a coroutine, it is immediately propagated to parent coroutines. Without waiting for anything.

If you use CoroutineStart.LAZY then the code inside async() is not started automatically, it waits for await() or something similar. So it doesn’t throw, because it doesn’t even run to the point where exception happen.

1 Like

Same here. I think the whole machinery of coroutines is confusing.

The following code throws an exception

 runBlocking {
            async {
                throw ArithmeticException()
            }
        }

Now, if we add a scope to async it will not throw an exception

 runBlocking {
            lifecycleScope.async {
                throw ArithmeticException()
            }
        }

By “confused” I meant that I’m not sure what is unclear to you :wink:

It throws exception in both cases. But in the second case you changed the scope of the coroutine, so it is entirely detached from the runBlocking(). It throws exception inside lifecycleScope, not in runBlocking().

This is not really specific to coroutines, but to processing asynchronous tasks in general. Usually, tasks are structured as a tree where each task can consists of multiple subtasks. In coroutines, failed subtask automatically fails the parent task. So the question is how you strucuture these tasks. In your first example the coroutine started in async() is a subtask of runBlocking(). In the second example, it is a subtask of lifecycleScope. runBlocking() doesn’t wait for it, it doesn’t care about it. As a matter of fact, runBlocking() in this example really does nothing and could/should be removed.

1 Like

I did some work on exception handling in coroutines. Here I documented it

You just asked how exception handling works with coroutines and now you teach others about it? No offense, but don’t you think you risk spreading misinformation?

I personally don’t like the general thought in the article that coroutines somehow change the way how exceptions work. Coroutines can’t and don’t do that, exceptions generally work as usual, but coroutines automatically handle their propagation to parents.

  • The whole “When exception is thrown?” is confusing to me. You say throw inside a root async() “Will not throw the exception”. Of course it still throws, even if we don’t use await().
  • The behavior of try ... catch is just standard, it doesn’t behave differently with coroutines than non-coroutine code. Both your examples (inside and outside launch()) represent just standard behavior of try ... catch.
  • Suggesting that coroutineScope() is a solution to the problem of catching exceptions from launch() doesn’t make too much sense. These two functions do much different things and you choose between them depending on what your code should do, not how it should handle exceptions.
  • I don’t have a strong opinion about this, but I think async(Job()) in most cases is a misuse of coroutines. Single launch() inside coroutineScope() is definitely a misuse.
2 Likes

Thank you @broot for you kind suggestions. I am a beginner in Kotlin/Coroutines and still learning it. I just did some work and documented it. I am not claiming that this is the final and perfect article on coroutines/exceptions. There is quite possibility that some points/observations are specific to my code example and are not generic in nature. I am just sharing with the intension that may be someone get some benefit. Will try to improve it in future. Thank yo once again for your guidance and suggestions.