Differences of Suspend Functions with Retrofit on Exceptions

Bram Yeh
2 min readFeb 13, 2020

--

When our projects adopted coroutine and suspend functions with Retrofit v2.6 above, we found there are some differences between the response types and the root causes of why the error happened.

Let’s define two different responded types of Retrofit interface, one is ObjectService and it prefers the unmixed objects, and the other is ResponseService which returns retrofit2.Response.

interface ObjectService {
@GET("/")
suspend fun getString(): String?
}

interface ResponseService {
@GET("/")
suspend fun getString(): Response<String?>
}

And we write the unit tests to see what’s different.

@get:Rule
val server = MockWebServer()

For the ObjectService, when the API responses the error code of 400, suspend fun getString(): String? throws the exception.

val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
// This is because we didn't use Gson or others
.addConverterFactory(StringConverterFactory())
.build()

val service = retrofit.create(ObjectService::class.java)
server.enqueue(MockResponse().setResponseCode(400).setBody("Hi"))

try {
service.getString()
Assert.fail()
} catch (e: Exception) {
Assert.assertEquals("HTTP 400 Client Error", e.message)
}

Otherwise, when the ResponseService‘s API reponses the error code of 400, suspend fun getString(): Response<String?> won’t fire exceptions, instead, it will package the error code into retrofit2.Response.

val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(StringConverterFactory())
.build()

val service = retrofit.create(ResponseService::class.java)
server.enqueue(MockResponse().setResponseCode(400).setBody("Hi"))

val result = service.getString()
Assert.assertEquals(400, result.code())
Assert.assertEquals(false, result.isSuccessful)
Assert.assertEquals(null, result.body())

However, suspend fun getString(): Response<String?> might still throw exceptions, for example, the SocketTimeoutException.

val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(StringConverterFactory())
.build()

val service = retrofit.create(ResponseService::class.java)
// we don't enque any reponse to emulate the timeout case
try {
service.getString()
Assert.fail()
} catch (e: Exception) {
Assert.assertEquals("timeout", e.message)
}

Since we needn’t respond code, so we use suspend fun getString(): String? to implement our all methods of Retrofit. To avoid uncaught exceptions, we use try-catch to wrap all the suspend methods.

However, it didn’t work, we still get java.net.SocketTimeoutException

Caused by java.net.SocketTimeoutException
failed to connect to xxxxxxx/xxx.xxx.xxx.xxx (port xxx) from /xxx.xxx.xxx.xxx (port xxx) after 10000ms

I will explain more details and the reason (we didn’t catch the exception) in the next story.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Bram Yeh
Bram Yeh

Written by Bram Yeh

Lead Android & iOS Mobile Engineer at Yahoo (Verizon Media) Taiwan https://www.linkedin.com/in/hanruyeh/

No responses yet

Write a response