Skip to content

Commit ee887a0

Browse files
dadachiclaude
andcommitted
Replace generic Exception with sealed ApiException hierarchy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6b0b639 commit ee887a0

4 files changed

Lines changed: 46 additions & 15 deletions

File tree

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataS
55
import com.nativeapptemplate.nativeapptemplatefree.model.*
66
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
77
import com.nativeapptemplate.nativeapptemplatefree.model.Login
8+
import com.nativeapptemplate.nativeapptemplatefree.network.ApiException
89
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
910
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
1011
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
@@ -46,7 +47,7 @@ class LoginRepositoryImpl @Inject constructor(
4647
emit(true)
4748
}.suspendOnFailure {
4849
clearUserPreferences()
49-
throw Exception(message())
50+
throw ApiException.UnprocessableError(rawMessage = message())
5051
}
5152
}.flowOn(ioDispatcher)
5253

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.network
2+
3+
/**
4+
* Base exception for API errors thrown by repository implementations.
5+
*/
6+
sealed class ApiException(message: String, cause: Throwable? = null) : Exception(message, cause) {
7+
8+
/**
9+
* The API returned a structured error response that was successfully deserialized.
10+
*/
11+
class ApiError(
12+
val code: Int,
13+
val apiMessage: String,
14+
) : ApiException("$apiMessage [Status: $code]")
15+
16+
/**
17+
* The API returned an error response that could not be deserialized.
18+
*/
19+
class UnprocessableError(
20+
val rawMessage: String,
21+
cause: Throwable? = null,
22+
) : ApiException("Not processable error($rawMessage).", cause)
23+
}

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ inline fun <reified T : Any> throwApiError(
5353
}
5454

5555
if (nativeAppTemplateApiError != null) {
56-
throw Exception("${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]")
56+
throw ApiException.ApiError(
57+
code = nativeAppTemplateApiError.code,
58+
apiMessage = nativeAppTemplateApiError.message,
59+
)
5760
} else {
58-
throw Exception("Not processable error($errorMessage).")
61+
throw ApiException.UnprocessableError(rawMessage = errorMessage)
5962
}
6063
}

app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.junit.Assert.assertEquals
88
import org.junit.Assert.assertTrue
99
import org.junit.Test
1010
import kotlin.test.assertFailsWith
11+
import kotlin.test.assertIs
1112

1213
class ApiResponseExtensionsTest {
1314

@@ -26,45 +27,48 @@ class ApiResponseExtensionsTest {
2627
}
2728

2829
@Test
29-
fun emitApiResponse_onFailure_throwsException() = runTest {
30+
fun emitApiResponse_onFailure_throwsUnprocessableError() = runTest {
3031
val response = ApiResponse.Failure.Exception(Exception("network error"))
31-
assertFailsWith<Exception> {
32+
val exception = assertFailsWith<ApiException.UnprocessableError> {
3233
flow { emitApiResponse<String>(response) }.first()
3334
}
35+
assertEquals("network error", exception.rawMessage)
3436
}
3537

3638
@Test
37-
fun emitApiResponse_withTransform_onFailure_throwsException() = runTest {
39+
fun emitApiResponse_withTransform_onFailure_throwsUnprocessableError() = runTest {
3840
val response = ApiResponse.Failure.Exception(Exception("network error"))
39-
assertFailsWith<Exception> {
41+
val exception = assertFailsWith<ApiException.UnprocessableError> {
4042
flow { emitApiResponse<String, Boolean>(response) { true } }.first()
4143
}
44+
assertEquals("network error", exception.rawMessage)
4245
}
4346

4447
@Test
45-
fun throwApiError_includesErrorMessageInException() {
48+
fun throwApiError_throwsUnprocessableError_withMessage() {
4649
val response: ApiResponse<String> = ApiResponse.Failure.Exception(Exception("timeout"))
47-
val exception = assertFailsWith<Exception> {
50+
val exception = assertFailsWith<ApiException.UnprocessableError> {
4851
throwApiError(response, "timeout")
4952
}
53+
assertEquals("timeout", exception.rawMessage)
5054
assertEquals("Not processable error(timeout).", exception.message)
5155
}
5256

5357
@Test
54-
fun emitApiResponse_onFailure_exceptionContainsNotProcessableError() = runTest {
58+
fun emitApiResponse_onFailure_exceptionIsApiException() = runTest {
5559
val response: ApiResponse<String> = ApiResponse.Failure.Exception(Exception("server error"))
56-
val exception = assertFailsWith<Exception> {
60+
val exception = assertFailsWith<ApiException> {
5761
flow { emitApiResponse<String>(response) }.first()
5862
}
59-
assertTrue(exception.message!!.contains("Not processable error"))
63+
assertIs<ApiException.UnprocessableError>(exception)
6064
}
6165

6266
@Test
63-
fun emitApiResponse_withTransform_onFailure_exceptionContainsNotProcessableError() = runTest {
67+
fun emitApiResponse_withTransform_onFailure_exceptionIsApiException() = runTest {
6468
val response: ApiResponse<String> = ApiResponse.Failure.Exception(Exception("server error"))
65-
val exception = assertFailsWith<Exception> {
69+
val exception = assertFailsWith<ApiException> {
6670
flow { emitApiResponse<String, Boolean>(response) { true } }.first()
6771
}
68-
assertTrue(exception.message!!.contains("Not processable error"))
72+
assertIs<ApiException.UnprocessableError>(exception)
6973
}
7074
}

0 commit comments

Comments
 (0)