Coroutines Playground: Concurrency + Timeout + Exception Handling

Mangesh Yadav
4 min readJun 7, 2023

You may have read about different concepts of coroutines, This article will put all those concepts together (okay not all but some 😉). We are going to learn about Coroutines using a problem statement.

Photo by Tony Pepe on Unsplash

Problem Statement

You have APIs to resolve User Details (User), User Friends List (List<Friends> ) and User Activity List ( List<UserActivity> ).

To get the user, call the getUserData() function. To get a user friends list and user activity list using id resolved from User.

All these functions are suspended and need to be called from a coroutine context.

Implement a function fetchData() where it must satisfy the following requirements:

  1. When fetchUserData() throws an error, handle the exception.
  2. When fetchUserData() return User , Call fetchFriends() and fetchUserActivity() using user.id then aggregate the result.
  3. When fetchFriends() call takes longer than 2000ms, and return a empty friends array.
  4. When fetchFriends() throws an exception, and return a empty friends array.
  5. When fetchUserActivity() call takes longer than 2000ms, returns an empty array.
  6. When fetchUserActivity() throws an exception, return an empty user activity array.

Solution

Note : Here API call functions are mocked with dummy data for demonstration purpose

First, we get user data using fetchUserData() with a dummy, a user is returned to simulate an API call. Added try catch for any handling exception that comes while making the fetchUserData() call.

class CoroutinePlayground {

suspend fun fetchData() {
try {
val result = withContext(Dispatchers.IO) { fetchUserData() }
} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}

private suspend fun fetchUserData(): User {
delay(1000)
return User()
}
}

// demo user
data class User(val name: String = "Demo user", val id: String = "123")

Next, let’s resolve fetchFriends() and fetchUserActivity() using the user data we got from fetchUserData() . Added the calls in supervisorScope and to achieve concurrency we are making use of async .

why supervisorScope ?: Unlike coroutineScope, a failure of a child does not cause this scope to fail and does not affect its other children.

class CoroutinePlayground {

suspend fun fetchData() {
supervisorScope {
try {
val user = withContext(Dispatchers.IO) { fetchUserData() }

val activityResult = async { fetchUserActivity(user.id) }
val friendResult = async { fetchFriends(user.id) }

} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
}

private suspend fun fetchUserData(): User {
delay(1000)
return User()
}

// made to simulate the api call for fecthing user's activity list
private suspend fun fetchUserActivity(id: String): List<UserActivity> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<UserActivity>().apply {
add(UserActivity("like"))
add(UserActivity("comment"))
}
}
}

// made to simulate the api call for fecthing user's friend list
private suspend fun fetchFriends(id: String): List<Friends> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<Friends>().apply {
add(Friends("john"))
add(Friends("jack"))
}
}
}
}

// demo Friends
data class Friends(val name: String)

// demo user activity
data class UserActivity(val type: String)

// demo user
data class User(val name: String = "Demo user", val id: String = "123")

The next step requires timeout and exception handling when making the fetchFriends() . To cancel a Coroutine in case of a timeout we can make use of withTimeout(time) . withTimeout() throws TimeoutCancellationException on timeout.

class CoroutinePlayground {

suspend fun fetchData() {
supervisorScope {
try {
val user = withContext(Dispatchers.IO) { fetchUserData() }

val activityResult = async { fetchUserActivity(user.id) }
val friendResult = async { fetchFriends(user.id) }

val friendList = try {
withTimeout(2000) {
friendResult.await()
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
} catch (e: Exception) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
}

} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
}

private suspend fun fetchUserData(): User {
delay(1000)
return User()
}

// made to simulate the api call for fecthing user's activity list
private suspend fun fetchUserActivity(id: String): List<UserActivity> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<UserActivity>().apply {
add(UserActivity("like"))
add(UserActivity("comment"))
}
}
}

// made to simulate the api call for fecthing user's friend list
private suspend fun fetchFriends(id: String): List<Friends> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<Friends>().apply {
add(Friends("john"))
add(Friends("jack"))
}
}
}
}

// demo Friends
data class Friends(val name: String)

// demo user activity
data class UserActivity(val type: String)

// demo user
data class User(val name: String = "Demo user", val id: String = "123")

Similarly, we can handle timeout and exceptions forfetchUserActivity() API call.

class CoroutinePlayground {

suspend fun fetchData() {
supervisorScope {
try {
val user = withContext(Dispatchers.IO) { fetchUserData() }

val activityResult = async { fetchUserActivity(user.id) }
val friendResult = async { fetchFriends(user.id) }

val friendList = try {
withTimeout(2000) {
friendResult.await()
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
} catch (e: Exception) {
Log.e(TAG, "friendResult: ${e.message}")
emptyList()
}

val activityList = try {
withTimeout(2000) {
activityResult.await()
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "activityList: ${e.message}")
emptyList()
} catch (e: Exception) {
Log.e(TAG, "activityList: ${e.message}")
emptyList()
}

Log.d(TAG, "friendList: ${friendList.size}")

Log.d(TAG, "activityList: ${activityList.size}")

} catch (e: Exception) {
Log.e(TAG, "fetchData: ${e.message}")
}
}
}

private suspend fun fetchUserData(): User {
delay(1000)
return User()
}

// made to simulate the api call for fecthing user's activity list
private suspend fun fetchUserActivity(id: String): List<UserActivity> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<UserActivity>().apply {
add(UserActivity("like"))
add(UserActivity("comment"))
}
}
}

// made to simulate the api call for fecthing user's friend list
private suspend fun fetchFriends(id: String): List<Friends> {
return withContext(Dispatchers.IO) {
return@withContext mutableListOf<Friends>().apply {
add(Friends("john"))
add(Friends("jack"))
}
}
}
}

// demo Friends
data class Friends(val name: String)

// demo user activity
data class UserActivity(val type: String)

// demo user
data class User(val name: String = "Demo user", val id: String = "123")

And this code demonstrates the solution for the above problem statement.

To simulate the exception handling and timeout behavior, you try throwing an exception from fetchUserData() , fetchUserActivity() and fetchFriends() .

You can also add a delay in fetchUserActivity() and fetchFriends() to see the timeout behavior.

Hope this article helped you 😄

Feel free to provide any feedback, Happy coding :)

Read about object expression in Kotlin

--

--