Skip to main content

Error Handling

It uses the Either type from the Arrow library to represent a value of one of two possible types, typically used for error handling.

The handleCall function is a suspend function that takes an apiCall of type ApiCall<T>, a mapper function to transform the response data, and an optional onDataNull function to handle cases where the response data is null. The function executes the API call and processes the response:

suspend fun <T, R> handleCall(
apiCall: ApiCall<T>,
mapper: suspend (T, String?, Int?) -> R,
onDataNull: (AIAResponse<T>?) -> Either<DataException, R> = { ... }
): Either<DataException, R> = try {
apiCall().let { ... }
} catch (e: Exception) {
e.printStackTrace()
e.convertEither()
}

If the API call is successful, it checks the errorCode in the response body. If the errorCode is one of the expected values (0, 119, 405), it either maps the response data using the provided mapper function or calls onDataNull if the data is null. If the errorCode is different, it calls the errorResponseHandler function to handle the error:

when (body?.errorCode) {
0, 119, 405 -> { ... }
else -> { errorResponseHandler(it, body?.message) }
}

The errorResponseHandler function processes the error response. It attempts to parse the error body as a JSON object to extract the error code and message. Depending on the error code, it returns an appropriate DataException wrapped in an Either.Left:

suspend fun <T, R> errorResponseHandler(
it: Response<T>,
message: String?
): Either<DataException, R> { ... }

For example, if the error code is 503, it emits a maintenance state using GlobalStateFlow and returns a DataException.Api with an empty message. If the error code is 401 or 418, it returns a DataException.Api with the corresponding error message.

The convertEither extension function on Exception converts different types of exceptions into DataException instances. For instance, network-related exceptions are converted to DataException.Network, while serialization exceptions are converted to DataException.Api with a specific error message:

private fun Exception.convertEither(): Either<DataException, Nothing> = when (this) {
is NetworkErrorException, is UnknownHostException -> Either.Left(DataException.Network)
is SerializationException -> { ... }
else -> { ... }
}

This code structure ensures that API responses and errors are handled consistently, providing a clear separation between successful and erroneous outcomes.

Complete code block

AIAResponse.kt

package com.aiamm.data.model.response

import kotlinx.serialization.SerialName

@kotlinx.serialization.Serializable
class AIAResponse<T>(
@SerialName("message")
val message: String?,

@SerialName("code")
val errorCode: Int?,

@SerialName("data")
val data: T?
)

ResponseHandler.kt

package com.aiamm.network.handler

import android.accounts.NetworkErrorException
import arrow.core.Either
import com.aiamm.common.GlobalStateFlow
import com.aiamm.data.model.response.AIAResponse
import com.aiamm.domain.exception.DataException
import java.net.UnknownHostException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.SerializationException
import org.json.JSONObject
import retrofit2.Response

typealias ApiCall<T> = suspend () -> Response<AIAResponse<T>>

const val ERROR_MESSAGE_GENERAL = "Something went wrong. Please try again."
const val ERROR_JSON_CONVERSION = "Error json conversion. Please try again."
const val ERROR_TITLE_GENERAL = "Error"

suspend fun <T, R> handleCall(
apiCall: ApiCall<T>,
mapper: suspend (T, String?, Int?) -> R,
onDataNull: (AIAResponse<T>?) -> Either<DataException, R> = {
Either.Left(
DataException.Api(
title = ERROR_TITLE_GENERAL,
message = it?.message ?: ERROR_MESSAGE_GENERAL,
errorCode = it?.errorCode ?: -1
)
)
}
): Either<DataException, R> = try {
apiCall().let {
it.body().let { body ->
when (body?.errorCode) {
0, 119, 405 -> {
if (body.data != null) {
Either.Right(mapper(body.data!!, body.message.orEmpty(), body.errorCode))
} else {
onDataNull(body)
}
}

else -> {
errorResponseHandler(it, body?.message)
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
e.convertEither()
}

suspend fun <T, R> errorResponseHandler(
it: Response<T>,
message: String?
): Either<DataException, R> {

val jsonObject: JSONObject? = try {
suspendCancellableCoroutine { cont ->
runCatching {
cont.resume(it.errorBody()?.string())
}.recover { throwable ->
cont.resumeWithException(throwable)
}
}?.let { JSONObject(it) }
} catch (e: Exception) {
null
}

val errorCode = try {
jsonObject?.getInt("Code")
} catch (e: Exception) {
null
}

val errorMessage = try {
jsonObject?.getString("Message")
} catch (e: Exception) {
null
}

return when {
it.code() == 503 -> {
val (maintenanceOn, title, description) = if (jsonObject != null) {
Triple(
jsonObject.getBoolean("MaintenanceOn"),
jsonObject.getString("Title"),
jsonObject.getString("Description")
)
} else Triple(true, "", "")

GlobalStateFlow.emit(
GlobalStateFlow.GlobalState.Maintenance(
maintenanceOn = maintenanceOn,
title = title.orEmpty(),
description = description.orEmpty()
)
)
Either.Left(DataException.Api(message = ""))
}

errorCode == 401 -> {
Either.Left(DataException.Api(message = ""))
}

errorCode == 418 -> {
Either.Left(DataException.Api(message = errorMessage ?: ERROR_MESSAGE_GENERAL))
}

else -> {
Either.Left(
DataException.Api(
message = message ?: errorMessage ?: ERROR_MESSAGE_GENERAL,
title = ERROR_TITLE_GENERAL,
errorCode = errorCode ?: -1
)
)
}
}
}

private fun Exception.convertEither(): Either<DataException, Nothing> =
when (this) {
is NetworkErrorException, is UnknownHostException -> Either.Left(DataException.Network)
is SerializationException -> {
Either.Left(
DataException.Api(
message = this.message ?: ERROR_JSON_CONVERSION,
title = ERROR_TITLE_GENERAL,
errorCode = -1
)
)
}

else -> Either.Left(
DataException.Api(
message = this.message ?: ERROR_MESSAGE_GENERAL,
title = ERROR_TITLE_GENERAL,
errorCode = -1
)
)
}