I have a coroutine in a coroutine where latter one...
# coroutines
l
I have a coroutine in a coroutine where latter one will rethrow a custom exception like:
Copy code
// use case
launch { repoFunc() } // try catch does not work because exceptions are propagated, but not rethrown

// repo
suspend fun repoFunc(scope: CoroutineScope) {
  ...
   scope.launch {
      // operation lives as long as application and is only canceled by timeout
      try {
          withTimeout { someOperation() }
      } catch(e: CancellationTimeoutException) {
          throw CustomException("Timeout") // How to catch this in the parent?
      }
   }
}
My question: Are a global CoroutineExceptionHandler,
coroutineScope
and
supervisorScope
my only options to catch the
CustomException
in the parent coroutine?
j
Is it not easier using something like Either instead of throwing your own exceptions?
l
Either is not part of Kotlin. I guess it's something like success/failure? Anyway the blog post is really good, but it doesn't cover 100% my use case so I'm not sure how to handle it properly. It's neither a logic error nor am I returning a type-safe result. I could handle it like a type-safe result and introduce a sealed class, but not sure? --> might be violate this "Don’t use exceptions as a work-around to sneak a result value out of a function." Another question would be what Roman means with this part:
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
Which architectural component of android would fit here? ViewModel, use case? Just for completeness: The code snippet above is part of my repo. Caller is use case. Edit: I have adjusted my code snippet.
e
An error happening and having a meaning in your code base is business logic. In general, business logic goes in use cases.
Introducing a sealed hierarchy including a type (or multiple) for your error state is exactly what is a good solution. It means that you no longer have to instantiate and throw exceptions. Instantiating exceptions can be costly (depending on the how you do that, Google it) and catching exceptions can also be costly (again, Google it). It's likely faster and much easier to read and understand, and certainly more idiomatic Kotlin if you materialise your error(s)!
👍🏻 1
So, in your example: • If the repo returns data or not, then name the function something like
dataOrNull(): Data?
and return null if no data • If the repo also returns errors having meaning in your business logic, then have the repo function return a sealed type, being a data or error type that you know. • The logic to handle the user's use case goes in the use case, e.g. to map the repo data, null or error to some model that the view model can map to a UI state and/or events
• If the repo returns data or it throws, then catch and handle the exceptions in the use case. Note: only catch expected exceptions for your business logic!! Repo implementation details like IO exceptions for disk or network access should likely not leak into the use case, so those should likely never reach the use case, but instead be handled by the repo.
👍 1
j
Either is a simple sealed class with two generics, similar to result but it allows custom error instead of only exceptions.
e
I prefer using a sealed hierarchy with 2 concrete types for that, because you achieve the same effect without the verbosity of the generics. With the either you likely have to write some useful operators too, which arguably lead to more verbosity at the call site. With sealed you get those for free in logical expressions (smart casting). There might be good use cases for an either class that I'm unaware of, but it seems just a little more verbose to me.
j
Have you an example about that?
In my mind you are talking about Either if you have an error or null if you don't need that error with the
...OfNull()
e
An example of the verbosity, or the smart casting?
The verbosity is arguable, of course. You don't need to implement operators for an either class, and they're not there for a sealed class too, unless you write them yourself
j
In my case I have a lot of operators for my either in multiplatform, and the arrow either has more, I think it is not a problem if you use it a lot
e
Another theoretical benefit of a sealed hierarchy is that it is mutable: you can simply add, change or remove a type and all exhaustive usages will be required to handle the changed type(s). That's a bit more difficult with either, I imagine?
j
It depends on the use case, you can have sealeds inside the either too, at the end it accepts generics
e
True, although it can be an extra step to unwrap the left or right value of the either
l
@Erik Thank you very much for your detailed answer. I won't rethrow any exceptions anymore. So let me recap: • its fine to catch exceptions in data layer/ repo • it's also fine to catch data layer exceptions in use case if it have meaning in my business logic --> in reference to: "If the repo returns data or it throws, then catch and handle the exceptions in the use case." • no rethrow of exceptions. Instead return appropriate error types based on a sealed class hierarchy Can you confirm that?
j
use cases should be free of exceptions if you have repositories
👍 1
third party libraries, usually used in data layer, will launch the exceptions, you have to catch them immediately
so in the same datasource, or the repo, or the use case, in that order
l
I read the blog post again and again and it's still not clear to me what responsibility
use cases
have regarding error handling or where I should catch exceptions. Just to clarify the terms: • Error handling = catching exceptions • domain layer = use cases • data layer = repository, data access/data source classes Meanwhile I can specify on what I don't understand. For example, lets assume my repository class has multiple functions. All of them represent a single operation on a device/network/database. Should I catch possible exceptions in every function or just let them pass to handle it in top-level code like Roman suggests in his blog post? Roman never mentioned this case explicitly. He just suggests to handle it uniformly in top-level. Next question would be, what if a repo function can throw multiple exceptions, should I catch every single exception explicitly or just
Exception
? I didn't forget what you say @Erik about using sealed classes here. I understand what you mean but at first I need to know where to catch (locally or in top-level like Roman suggests) and in what granularity I have to catch (just
Exception
or every single exception explicitly) Be aware, that every function of repository can call functions of a more abstract layer which in turn can also throw exceptions, e.g. repository function delegates call to data source function. If I catch them all explicitly in repository I would violate against what Roman suggests: "*you should not be catching exceptions in general Kotlin code."* You see, it's the exact opposite of what Roman is telling in his blog post and what confuses me. Further what about use cases and catching exceptions? Yes or No? @Erik In the first place you write:
If the repo returns data or it throws, then catch and handle the exceptions in the use case. Note: only catch expected exceptions for your business logic!!
but on the other hand you confirm this:
use cases should be free of exceptions if you have repositories
This is inconsitent. Roman brings up follwoing code snippet:
Copy code
fun updateOrderQuanity(orderId: OrderId, quantity: Int) {
    require(quantity > 0) { "Quantity must be positive" }
    val order = loadOrder(orderId)
    order.quantity = quantity
    storeOrder(order)
}
This is obviously business logic. You see, he is throwing an exception which implies that it has to be catched in the contiugous layer (or one that is further up the hierarchy) which would be viewmodel/presenter. Strictly speaking he says:
Dedicate a single top-level place in your code to address all network errors uniformly. This is usually done at a boundary between low-level logic of the code and high-level user-interface or service endpoint.
So according to Roman, this is wrong:
use cases should be free of exceptions if you have repositories
But even if I follow Roman's advices, I still don't know how it should be implemented because if I handle all exceptions in top-level (viewmodel/presenter) I can't be explicitly about the exceptions, otherwise I would leak API details into presentation layer. With explicitly I mean I need to do something like:
Copy code
// in viewmodel

when(throwable) {
  is BluetoothConnectionError -> TODO() // repeat some operation
  is HttpErrorWhatever -> TODO() // show error message
  is AnotherApiError -> TODO() // show error message
}
Thanks in advance!
j
Just catch exceptions where they appear (in the place where you use the third party libraries) if your app should not crash. Don't launch your own exceptions if they need to be catched. Some libraries use exceptions but your app should not crash for ALL of them. For example you are not going to catch a IndexOutofbounds exception if you know it shouldn't happen because is a bug in your code. Meanwhile you will catch httpexceptions from retrofit because your app shouldn't crash if there is a 404
Because in Kotlin there are no checked exceptions, why are you going to delegate the responsability to the ViewModel to catch them? A simple case could be: • Developer A creates a repo which uses some third party libraries to do whatever thing, and them throw exceptions. They are not caught there because the same developer create a ViewModel for ONE screen and they are caught there. • Developer B just want to use that repo in a new feature. That dev is going to check the internal of that repo to know there are exceptions and/or check the ViewModel created by dev A. Why this error prone system?
Alternative: • Developer A creates a repo which uses some third party libraries to do whatever thing, and them throw exceptions. They are caught directly in that repo with a mechanism that you will see directly when you use the function (Either, nulls, and so on). • Developer B have to check no code to know that repo can have errors and that dev is forced to check EVERY error, if not, the code isn't compiling.
And there is more, a custom error allows you to know what error is, for example, using a sealed class to indicate 4 or 5 errors. Propagating exceptions no, you can't know how many branches your exception handler will have in the ViewModel (first approach) without checking the code, 3 error branches? 10 branches?. If you use the second approach with a custom sealed class specifying the error should be caught, you can't fail, you will know there are X branches and you will have to do a solution for all of them.
l
Thanks for your investigation. I absolutely confirm what you say but it doesn't answer my questions. For example, how would you handle this use case:
Copy code
fun updateOrderQuanity(orderId: OrderId, quantity: Int) {
    require(quantity > 0) { "Quantity must be positive" }
    val order = loadOrder(orderId)
    order.quantity = quantity
    storeOrder(order)
}
It throws an exception if input is zero or negative.
require(quantity > 0)
is business logic so you won't move this code to another layer
j
It depends a lot, if the UI doesn't allow set 0 or less, I just let crash the app because there is a critical problem somewhere. If the UI allows setting 0 or less, I would use Arrow's Either
e
You could move the quantity through a validator that returns `true`/`false` before calling this function. Or call it
fun Int.validQuantityOrNull(): Int? = takeIf { it > 0 }
Or return a
UpdateOrderQuantityResult
from your function that indicates any error
j
If you want to be safe 100%, go for Either in both cases
e
By the SOLID principles you could argue that your function has too many responsibilities. Not only does it update the order quantity, but it also might do something else if the quantity is invalid. So it doesn't always do what the name suggests. So either the name is bad, or the behaviour is. I say the latter.
j
Or even a simple
enum class QuantityValidationResult { Success, Error }
e
Another solution in this particular case: use
UInt
instead of
Int
. Edit: this won't work, because
UInt
can be
0
which isn't allowed in your logic, apparently
Lol, Javier and I are spamming various similar options 😉
🙂 1
l
Correct me if I'm wrong but when I don't change the behavior:
Copy code
fun updateOrderQuanity(orderId: OrderId, quantity: Int) {
    require(quantity > 0) { "Quantity must be positive" }
    val order = loadOrder(orderId)
    order.quantity = quantity
    storeOrder(order)
}
and follow
Or return a 
UpdateOrderQuantityResult
 from your function that indicates any error
then I have to wrap
require
with a try catch block in order to return a result right? So I'm asking: do you suggest to change the behavior of the business logic so an exception will never be thrown or do you suggest to catch exceptions in use cases? Btw...the snippet is from blog post of Roman not mine.
I also think the naming of the function is correct. It's a bad design if I would return a result from this kind of function. That's also what Roman says in his blog post. I'm wondering why Javier posted the link of the blog post above but doing exactly the opposite of what is suggested there.
j
you are cherry picking things
"As a rule of thumb, you should not be catching exceptions in general Kotlin code."
Catching in the ViewModel is catching in Kotlin code IMO
the framework is not going to catch all exceptions because it is how Android works, other frameworks don't crash when there is an exception
"API design You should design your own general-purpose Kotlin APIs in the same way: use exceptions for logic errors, type-safe results for everything else. Don't use exceptions as a work-around to sneak a result value out of a function."
As I said: "It depends a lot, if the UI doesn't allow set 0 or less, I just let crash the app because there is a critical problem somewhere. If the UI allows setting 0 or less, I would use Arrow's Either"
My response said when you design the API you should throw exception that should not be caught, if it should be caught, then you should use an Either, a nullable, or whatever thing that allows you avoid throwing/catching the exception
I think you are looking for a black or white answer, and that is the problem, the answer can be gray