Lilly
05/23/2021, 2:05 PM// 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?Javier
05/23/2021, 2:21 PMJavier
05/23/2021, 2:21 PMLilly
05/23/2021, 3:09 PMAs 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 codeWhich 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.
Erik
05/23/2021, 3:25 PMErik
05/23/2021, 3:28 PMErik
05/23/2021, 3:33 PMdataOrNull(): 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 eventsErik
05/23/2021, 3:36 PMJavier
05/23/2021, 4:28 PMErik
05/23/2021, 5:58 PMJavier
05/23/2021, 6:00 PMJavier
05/23/2021, 6:01 PM...OfNull()
Erik
05/23/2021, 6:02 PMErik
05/23/2021, 6:03 PMJavier
05/23/2021, 6:08 PMErik
05/23/2021, 6:33 PMJavier
05/23/2021, 6:39 PMErik
05/23/2021, 7:39 PMLilly
05/25/2021, 10:04 PMJavier
05/25/2021, 10:35 PMJavier
05/25/2021, 10:35 PMJavier
05/25/2021, 10:36 PMLilly
06/02/2021, 1:00 AMuse 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 repositoriesThis is inconsitent. Roman brings up follwoing code snippet:
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 repositoriesBut 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:
// 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!Javier
06/02/2021, 7:10 AMJavier
06/02/2021, 7:17 AMJavier
06/02/2021, 7:21 AMJavier
06/02/2021, 7:24 AMLilly
06/02/2021, 4:00 PMfun 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 layerJavier
06/02/2021, 4:03 PMErik
06/02/2021, 4:04 PMfun Int.validQuantityOrNull(): Int? = takeIf { it > 0 }
Or return a UpdateOrderQuantityResult
from your function that indicates any errorJavier
06/02/2021, 4:04 PMErik
06/02/2021, 4:06 PMJavier
06/02/2021, 4:06 PMenum class QuantityValidationResult { Success, Error }
Erik
06/02/2021, 4:06 PMUInt
instead of Int
. Edit: this won't work, because UInt
can be 0
which isn't allowed in your logic, apparentlyErik
06/02/2021, 4:07 PMLilly
06/02/2021, 4:15 PMfun 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 athen I have to wrapfrom your function that indicates any errorUpdateOrderQuantityResult
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.Lilly
06/02/2021, 4:25 PMJavier
06/02/2021, 4:44 PMJavier
06/02/2021, 4:44 PMJavier
06/02/2021, 4:44 PMJavier
06/02/2021, 4:45 PMJavier
06/02/2021, 4:46 PMJavier
06/02/2021, 4:47 PMJavier
06/02/2021, 4:49 PMJavier
06/02/2021, 4:50 PM