Skip to main content

Kotlin Suspending Functions

When working with Kotlin Coroutines, suspending functions are one of the most important concepts to understand. They form the foundation of Kotlin's approach to asynchronous programming, allowing you to write code that is both efficient and easy to read.

What are Suspending Functions?

A suspending function is a special type of function that can pause its execution at certain points without blocking the thread it's running on. This capability enables non-blocking asynchronous programming in Kotlin.

Think of suspending functions as regular functions with an additional ability: they can "suspend" execution until a result is ready, and then continue where they left off.

Key Characteristics

  • Marked with the suspend keyword
  • Can call other suspending functions
  • Can only be called from coroutines or other suspending functions
  • Do not block threads while waiting for operations to complete

Basic Syntax

Here's the basic syntax for defining a suspending function:

kotlin
suspend fun myFunction() {
// Function body
}

The suspend modifier is what makes this function special - it tells the compiler that this function may suspend execution without blocking the thread.

How Suspending Functions Work

When a suspending function is called, it can perform some operations and then "suspend" or pause execution. The thread that was running the function is free to do other work. When the suspending point completes its work, the function resumes where it left off, potentially on a different thread.

Let's see a basic example:

kotlin
import kotlinx.coroutines.*

// A simple suspending function
suspend fun fetchData(): String {
delay(1000) // Suspends the coroutine for 1 second (non-blocking)
return "Data loaded"
}

fun main() = runBlocking {
println("Starting to fetch data...")
val data = fetchData() // Execution is suspended here without blocking the thread
println("Data received: $data")
}

Output:

Starting to fetch data...
Data received: Data loaded

In the above example:

  1. fetchData() is a suspending function that simulates a network request with a 1-second delay
  2. When fetchData() is called, the coroutine is suspended at the delay() call
  3. After 1 second, execution resumes and the function returns "Data loaded"

Suspension Points

A suspension point is any call to another suspending function within a suspending function. At these points, the function may yield its thread and suspend execution.

Common suspension points include:

  • delay(): Suspends the coroutine for a specified time
  • await(): Suspends until an asynchronous operation completes
  • withContext(): Switches to a different coroutine context and suspends
  • Calls to other suspending functions

Calling Suspending Functions

Suspending functions can only be called from:

  1. Another suspending function
  2. Within a coroutine builder like launch, async, runBlocking, etc.

They cannot be called from regular functions.

kotlin
// Regular function - CANNOT call suspending functions directly
fun regularFunction() {
// fetchData() // Error: Suspending function called from a non-suspending context
}

// Suspending function - CAN call other suspending functions
suspend fun anotherSuspendingFunction() {
val data = fetchData() // This is allowed
}

// Using a coroutine builder to call a suspending function
fun usingCoroutineBuilder() {
GlobalScope.launch {
val data = fetchData() // This is allowed inside a coroutine
}
}

Practical Examples

Example 1: Sequential API Calls

One common use case for suspending functions is making sequential API calls:

kotlin
suspend fun getUserProfile(userId: String): Profile {
val userDetails = fetchUserDetails(userId) // First API call
val userPreferences = fetchUserPreferences(userId) // Second API call
return Profile(userDetails, userPreferences)
}

suspend fun fetchUserDetails(userId: String): UserDetails {
delay(1000) // Simulate network request
return UserDetails(userId, "John Doe", "[email protected]")
}

suspend fun fetchUserPreferences(userId: String): UserPreferences {
delay(800) // Simulate network request
return UserPreferences(theme = "Dark", language = "English")
}

fun main() = runBlocking {
val profile = getUserProfile("user123")
println("User: ${profile.userDetails.name}")
println("Preferences: ${profile.userPreferences.theme} theme")
}

Output:

User: John Doe
Preferences: Dark theme

This code looks synchronous but actually performs asynchronous operations without blocking the thread.

Example 2: Parallel API Calls with async

We can improve the previous example by executing the API calls in parallel:

kotlin
suspend fun getUserProfileConcurrently(userId: String): Profile = coroutineScope {
val userDetailsDeferred = async { fetchUserDetails(userId) }
val userPreferencesDeferred = async { fetchUserPreferences(userId) }

// Both requests are running concurrently, but we wait for both to complete
val userDetails = userDetailsDeferred.await()
val userPreferences = userPreferencesDeferred.await()

Profile(userDetails, userPreferences)
}

fun main() = runBlocking {
val startTime = System.currentTimeMillis()

val profile = getUserProfileConcurrently("user123")

val endTime = System.currentTimeMillis()
println("User: ${profile.userDetails.name}")
println("Preferences: ${profile.userPreferences.theme} theme")
println("Time taken: ${endTime - startTime} ms") // Will be ~1000ms instead of ~1800ms
}

Output:

User: John Doe
Preferences: Dark theme
Time taken: 1020 ms

Example 3: Error Handling in Suspending Functions

Suspending functions can use traditional try-catch blocks for error handling:

kotlin
suspend fun fetchUserDataSafely(userId: String): UserData {
return try {
val details = fetchUserDetails(userId)
val preferences = fetchUserPreferences(userId)
UserData(details, preferences, success = true)
} catch (e: Exception) {
println("Error fetching user data: ${e.message}")
UserData(null, null, success = false)
}
}

fun main() = runBlocking {
val userData = fetchUserDataSafely("invalidUser")
if (userData.success) {
println("User data loaded successfully")
} else {
println("Failed to load user data")
}
}

Building Your Own Suspending Functions

You can create your own suspending functions to wrap callback-based APIs or blocking operations:

Example: Converting a Callback-based API to a Suspending Function

kotlin
// Traditional callback-based approach
fun fetchUserDataWithCallback(userId: String, callback: (UserData?, Error?) -> Unit) {
// Implementation of a traditional callback API
// ...
}

// Converted to a suspending function using suspendCoroutine
suspend fun fetchUserDataSuspend(userId: String): UserData = suspendCoroutine { continuation ->
fetchUserDataWithCallback(userId) { userData, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(userData!!)
}
}
}

// Now you can use it like a regular suspending function
fun main() = runBlocking {
try {
val userData = fetchUserDataSuspend("user123")
println("User data: $userData")
} catch (e: Exception) {
println("Error: ${e.message}")
}
}

Example: Making Blocking Calls Non-blocking

kotlin
suspend fun readFileSuspending(filePath: String): String = withContext(Dispatchers.IO) {
// This code runs in an IO-optimized thread pool
File(filePath).readText() // Blocking I/O operation
}

fun main() = runBlocking {
val fileContent = readFileSuspending("config.txt")
println("File content: $fileContent")
}

Best Practices for Suspending Functions

  1. Keep functions focused: Each suspending function should perform a single task.
  2. Document suspension points: Make it clear where functions may suspend.
  3. Handle errors properly: Use try-catch blocks for error handling.
  4. Choose the right context: Use appropriate dispatchers for CPU-intensive or I/O-bound operations.
  5. Avoid blocking operations: Don't use Thread.sleep() or blocking I/O in suspending functions.

Common Pitfalls to Avoid

  1. Calling suspending functions from regular functions: Always call suspending functions from a coroutine or another suspending function.
  2. Blocking threads: Avoid using blocking calls inside suspending functions.
  3. Not handling exceptions: Always handle exceptions in suspending functions.
  4. Leaking coroutines: Always ensure coroutines are properly cancelled when no longer needed.

Summary

Suspending functions are a powerful feature of Kotlin's coroutines system that enable you to write asynchronous code in a sequential, readable manner. They allow your code to pause execution without blocking threads, making your applications more efficient and responsive.

Key points to remember:

  • Suspending functions are marked with the suspend modifier
  • They can only be called from other suspending functions or within coroutine builders
  • They enable non-blocking asynchronous code that looks synchronous
  • They form the foundation of Kotlin's approach to concurrency

Exercises

  1. Create a suspending function that simulates fetching weather data and includes a delay.
  2. Convert a callback-based API of your choice to use suspending functions.
  3. Write a function that performs three API calls concurrently and combines their results.
  4. Implement proper error handling for a chain of suspending function calls.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)