I can't seem to get my error-handling done in coroutines. I've been reading lots of articles and the exception handling documentation but I can't seem to get it working.
Here's my setup:
My ViewModel launches the coroutine with it's scope
class MyViewModel(private var myUseCase: MyUseCase) : ViewModel() {
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun doSomething() {
uiScope.launch {
try {
myUseCase()
} catch (exception: Exception) {
// Do error handling here
}
}
}
}
My UseCase just handles a few logic and in this case a validator of some sort
class MyUseCase(private val myRepository: MyRepository) {
suspend operator fun invoke() {
if (checker()) {
throw CustomException("Checker Failed due to: ...")
}
myRepository.doSomething()
}
}
Then my Repository just handles the network layer / local layer
object MyRepository {
private val api = ... // Retrofit
suspend fun doSomething() = api.doSomething()
}
And here's my Retrofit interface
interface MyInterface {
#POST
suspend fun doSomething()
}
The try/catch from the ViewModel can handle the error from the Retrofit call however, it can't catch the error from the CustomException thrown by the UseCase. From articles I've been reading, this should work. If I use async I can do await and consume the error but I don't have to use async in this case and I've been wrapping my head around this. I might be getting lost.
Any help would be greatly appreciated! Thanks in advance!
Edit:
Here's the error log I'm getting:
com.example.myapp.domain.errors.CustomException
at com.example.myapp.domain.FeatureOne$invoke$2.invokeSuspend(FeatureOne.kt:34)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
The error directly points to the explicit throw statement.
Trying with CoroutineExceptionHandler can be workaround for handling exceptions inside coroutines.
CoroutineExceptionHandler context element is used as generic catch block of coroutine where custom logging or exception handling may take place. It is similar to using Thread.uncaughtExceptionHandler.
How to use it?
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException() // Nothing will be printed, relying on user to call
deferred.await()
}
joinAll(job, deferred)
In your ViewModel, make sure that your uiScope is using SupervisorJob rather than Job. SupervisorJob's can handle its children's failure individually. Job would get cancelled unlike SupervisorJob
If you're using 2.1.0 for AAC Lifecycle and ViewModel, use the viewModelScope extension instead.
Another way to resolve this would be to covert your custom error object to implement CancellationException
For eg:
Your CustomException can be implemented as :
sealed class CustomError : CancellationException() {
data class CustomException(override val message: String = "Checker Failed due to: ...") : CustomError
}
This exception would get caught in the try/catch block of the view model
As far as I know Retrofit hasn't still created the way to mark the methods with the suspend keyword. You can refer it to this link.
So the correct way of your MyInterface would be:
interface MyInterface {
#POST
fun doSomething(): Deferred<Response<YourDataType>>
}
Related
I have emit exception inside flow and got below exception.
IllegalStateException: Flow exception transparency is violated:
Previous 'emit' call has thrown exception java.lang.NullPointerException, but then emission attempt of value 'planetbeyond.domain.api.Resource$Error#85b4d28' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
at kotlinx.coroutines.flow.internal.SafeCollector.exceptionTransparencyViolated(SafeCollector.kt:140)
at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:104)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:83)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
at planetbeyond.domain.use_cases.OptionSelectedCountUsecase$invoke$1.invokeSuspend(OptionSelectedCountUsecase.kt:20)
OptionSelectedCountUsecase.kt
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
emit(Resource.Success(data))
} catch (e: Exception) {
emit(Resource.Error(e.toString()))// crashed at this line when api don't response anything or some sort of server error
}
}
}
Repository.kt
interface Repository{
suspend fun getOptionSelectedCount(questionId: Int):List<OptionSelectedCountModel>
}
RepositoryImpl.kt
class RepositoryImpl #Inject constructor(
private val apiService: ApiService
) : Repository {
override suspend fun getOptionSelectedCount(questionId: Int): List<OptionSelectedCountModel> {
return apiService.getOptionSelectedCount(questionId).data.map {
it.toModel()
}
}
}
ApiService.kt
interface ApiService {
#GET("get_option_selected_count")
suspend fun getOptionSelectedCount(
#Query("question_id") question_id: Int
): WebResponse<List<OptionSelectedCountDto>>
}
LiveShowQuestionViewModel.kt
#HiltViewModel
class LiveShowQuestionsViewModel #Inject constructor(
private val optionSelectedCountUsecase: OptionSelectedCountUsecase
) : ViewModel() {
fun getOptionSelectedCount(questionId: Int) {
optionSelectedCountUsecase(questionId).onEach {
when (it) {
is Resource.Loading -> {
_optionSelectedCountState.value = OptionSelectedCountState(isLoading = true)
}
is Resource.Error -> {
_optionSelectedCountState.value = OptionSelectedCountState(error = it.message)
}
is Resource.Success -> {
_optionSelectedCountState.value = OptionSelectedCountState(data = it.data)
}
}
}///.catch { } // Why must I have to handle it here
.launchIn(viewModelScope)
}
}
Is it neccessary to handle exception outside flow like commented above. What is the best practice.
The problem is that you wrapped an emit call in try and try to emit in the matching catch block. This means that if the emit call itself throws (which ambiguously could be caused by some downstream problem with the flow) it's being instructing to emit again. This is very ambiguous and fragile behavior.
Instead, you can move your emit call(s) outside the try/catch:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
val result = try {
val data = repository.getOptionSelectedCount(questionId)
Resource.Success(data)
} catch (e: Exception) {
Resource.Error(e.toString())
}
emit(result)
}
}
Somehow, you're causing a NullPointerException in your collector. That's a separate problem to solve.
The root problem is that your
emit(Resource.Success(data))
throws an exception. When you catch that exception you are still in the "emit" block and you are trying to
emit(Resource.Error(e.toString())
So it's like emit inside emit. So yes this is wrong.
But let's get a step backward. Why there is an exception during the first emit? It looks like this data object is not properly filled with data, probably because of the issues that you mentioned (bad response etc), after it reaches the collector there is null pointer exception.
So basic flow should be
try to make the call, and catch http/parsing exception if there is one ( emit failure)
If there was no exception, validate if the object contains proper fields. If data is inconsistent emit Error
If everything is ok emit success
for example:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
emit(Resource.Success(data))
}else{
// some data integrity issues, missing fields
emit(Resource.Error("TODO error")
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
emit(Resource.Error(e.toString()))
}
}
}
This ideally should be split into, to not mess with exception catching of emit:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
emit(getResult(questionId))
}
fun getResult(questionId: Int): Resource<List<OptionSelectedCountModel>>{
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
return Resource.Success(data)
}else{
// some data integrity issues, missing fields
return Resource.Error("TODO error"
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
return Resource.Error(e.toString())
}
}
}
You should not emit exceptions and errors manually. Otherwise the user of the flow will not know, if exception actually happened, without checking the emitted value for being an error.
You want to provide exception transparency, therefore it is better to process them on collecting the flow.
One of the ways is to use catch operator. To simplify flow collecting we will wrap the catching behavior in a function.
fun <T> Flow<T>.handleErrors(): Flow<T> =
catch { e -> showErrorMessage(e) }
Then, while collecting the flow:
optionSelectedCountUsecase(questionId)
.onEach { ... }
.handleErrors()
.launchIn(viewModelScope)
Note, that if you want to process only the errors from invocation of the use case, you can change the order of operators. The previous order allows you to process errors from onEach block too. Example below will only process errors from use case invocation.
optionSelectedCountUsecase(questionId)
.handleErrors()
.onEach { ... }
.launchIn(viewModelScope)
Read more about exception handling in flows
I am making an api call using retrofit and I want to write a unit test to check if it returns an exception.
I want to force the retrofit call to return an exception
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
try {
val creditReport = apiServiceInterface.getDataFromApi() // THIS SHOULD RETURN AN EXCEPTION AND I WANT TO CATCH THAT
return CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
return CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
I have written a test case for getCreditReport which should validate the failure scenario
#Test
fun getCreditReportThrowException() {
runBlocking {
val response = dataRepository.getCreditReport()
verify(apiServiceInterface, times(1)).getDataFromApi()
Assert.assertEquals(CreditReportResponse.Status.FAILURE, response.status)
}
}
so to make the above test case pass, I need to force the network call to throw and exception
please suggest
Thanks
R
Actually #Vaibhav Goyal provided a good suggestion to make your testing as easier. Assuming you are using MVVM structure, in your test cases you can inject a "mock" service class to mock the behaviours that you defined in the test cases, so the graph will be like this
Since I am using mockk library at the moment, the actual implementation in your code base would be a little bit different.
#Test
fun test_exception() {
// given
val mockService = mockk<ApiServiceInterface>()
val repository = DataRepository(mockService)
every { mockService.getDataFromApi() } throws Exception("Error")
// when
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
}
But if you want to test the exception thrown from Retrofit, then you might need mockServer library from square to help you to achieve this https://github.com/square/okhttp#mockwebserver
And the graph for this would be like this
You also have to setup the mock server to do so
#Test
fun test_exception_from_retrofit() {
// can put in the setup method / in junit4 rule or junit5 class
val mockWebServer = MockWebServer()
mockWebServer.start()
// given
val service = Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.build()
.create(ApiServiceInterface::class)
val repository = DataRepository(service)
// when
mockWebServer.enqueue(MockResponse()
.setResponseCode(500)
.setBody("""{"name":"Tony}""") // you can read the json file content and then put it here
)
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
// can put in tearDown / in junit4 rule or juni5 class
mockWebServer.shutdown()
}
SO you can test different exception like json format invalid, 500 status code,data parsing exception
Bonus point
Usually I would put the testing json under test directory and make it almost same as the api path for better maintainence
My viewmodel calls the repository methods to fetch some data from room database and also from network.
class Repository #Inject constructor(
private val remoteDatasource: IRemoteSource,
private val localDatasource: ILocalSource,
private val subscriberScheduler: Scheduler,
private val observerScheduler: Scheduler
) : IRepository {
//this method fetches data from room
override fun getData(): Flowable<Boolean> {
return localDatasource.shouldFetchRemote().subscribeOn(subscriberScheduler)
.observeOn(observerScheduler)
}
// makes api call
override fun getRemoteData(): Flowable<Data> {
return remoteDatasource.getData().subscribeOn(subscriberScheduler)
.observeOn(observerScheduler)
}
subscriberScheduler is Schedulers.io() and observer scheduler is AndroidSchedulers.mainThread().
I get exception when I do query from room, saying that the opertion is in main thread.
Also when I get data from remote source, I check the thread, it is main thread, but this no exception like network call on main thread.
Here is my localsource class which uses room:
class Localsource constructor(private val dataDao: DataDao):ILocalSource {
override fun shouldFetchRemote(): Flowable<Boolean> {
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints
}
//exception thrown here
return Flowable.just(dataDao.isDataPresent() != 0)
}
Here is class for RemoteSource
#OpenForTesting
class Remotesource #Inject constructor():IRemoteSource{
override fun getData(): Flowable<Data> {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints but no exception is thrown like network call on main thread.
}
return service.getData().flatMap { Flowable.just(it.data) }
}
}
Your assumptions about what happens where are wrong. That is an issue.
Lets look at shouldFetchRemote() method.
//This part will always be on the main thread because it is run on it.
//Schedulers applied only for the created reactive
//stream(Flowable, Observable, Single etc.) but not for the rest of the code in the method.
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Log.v("thread","main thread")
//this log prints
}
//exception thrown here
//Yes it is correct that exception is thrown in this line
//because you do reach for the database on the main thread here.
//It is because Flowable.just() creates stream out of the independent data
//that does not know anything about scheduler here.
// dataDao.isDataPresent() - is run on the main thread
//because it is not yet part of the reactive stream - only its result is!!!!
//That is crucial
return Flowable.just(dataDao.isDataPresent() != 0)
In order to include the function into a stream you need to take another approach. Room has an ability to return Flowables directly and store booleans. This way you can use it like this
In DAO
#Query(...)
Boolean isDataPresent(): Flowable<Boolean>
In your local source
override fun shouldFetchRemote(): Flowable<Boolean> = dataDao.isDataPresent()
This way it will work as expected because now the whole function is the part of reactive stream and will react to schedulers.
The same with remote sourse. Retrofit can return Observables or Flowables out of the box
interface Service{
#GET("data")
fun getData(): Flowable<Data>
}
// and the repo will be
val service = retrofit.create(Service::class.java)
override fun getData(): Flowable<Data> = service.getData()
This way everything will work as expected because now it is the part of a stream.
If you want to use plan data from Room or Retrofit - you can do it either. The only thing is Flowable.just() won't work.
For example for your local source you will need to do something like
//DAO
#Query(...)
Boolean isDataPresent(): Boolean
override fun shouldFetchRemote(): Flowable<Boolean> = Flowable.create<Boolean>(
{ emitter ->
emitter.onNext(dataDao.isDataPresent())
emitter.onComplete() //This is crucial because without onComplete the emitter won't emit anything
//There is also emitter.onError(throwable: Throwable) to handle errors
}, BackpressureStrategy.LATEST).toObservable() // there are different Backpressure Strategies
There are similar factories for Obserwable and other reactive stream.
And generally I would recommend you to read the documentation.
i built my app with rxjava and needed to convert to coroutines for the background work i need help to see an actual example using retrofit 2.6.0 and coroutines and a proper error handling methods thanks in advance
i tried creating a generic error handler for retrofit and a wrapper for network resource states of Loading Authenticated Error
class AuthRepository #Inject constructor(val authApi: AuthApi) {
suspend fun Loginauth(email:String,password:String)= liveData {
emit(AuthResource.loading(null))
try{
val response=authApi.login(email,password)
if (response.isSuccessful){
emit(AuthResource.authenticated(response.body()))
}
emit(AuthResource.error(response.message(),null))
}catch (e:Throwable){
Log.d("any","error occured ${e.message}")
emit(AuthResource.error(e.message!!,null))
}catch (e:HttpException){
Log.d("any","error http error${e.message}")
emit(AuthResource.error(e.message!!,null))
}
}
}
My ViewModelClass
screenState.value= AuthResource.loading(null)
viewModelScope.launch {
val source=authRepository.Loginauth(email!!,password!!)
screenState.addSource(source){
AuthResource.authenticated(it)
screenState.removeSource(source)
if (it.status == AuthResource.AuthStatus.ERROR){
screenState.removeSource(source)
AuthResource.error(it.message!!,null)
}
}
}
}
I'm giving a try to Kotlin Coroutines inside an Android app, specifically I've imported Kotlin Coroutine Adapter for Retrofit.
Kotlin Coroutine Adapter changes Retrofit interface to return a Deferred<T> instead of Call<T>.
What I don't understand is how to launch this Deferred in a particular CoroutineContext that I want to. Consider following code:
class MyViewModel #Inject constructor(
private val foo: Foo,
#Named("ui") private val uiContext: CoroutineContext,
#Named("network") private val networkContext: CoroutineContext
) : ViewModel() {
fun performSomeJob(param: String) {
launch(uiContext) {
try {
val response = foo.bar(param).await()
myTextView.setText(response.name)
} catch (error: Throwable) {
Log.e(error)
}
}
}
Where foo.bar(param) returns Deferred<SomeModel>.
This code works, but I'm not sure on what CoroutineContext this foo.bar(param) is being executed (CommonPool??).
How to explicitly specify, that I want foo.bar(param) to be executed in a networkContext?
val response = async(networkContext) { foo.bar(param) }.await()
This code doesn't work, because response is evaluated to Deferred<SomeModel> instead of SomeModel (which I want to achieve).
The foo.bar() call doesn't start another coroutine, it just wraps the native Retrofit Call so that its state changes get propagated to Deferred. Retrofit manages its own threads to perform its operations and this works just as it would without the coroutine wrapper. If you have a specific concern, you can manage it by configuring Retrofit in the usual way.
The only thing that should matter to you is that your coroutine is executing in the UI context.