Question

Fixing "Inappropriate Blocking Method Call" in Kotlin Coroutines

kotlinokhttpkotlin-coroutinesmoshi

Question

I am trying to use Kotlin coroutines more consistently, but I am running into a warning when I call libraries such as Moshi or OkHttp inside a coroutine:

// example shape of the problem
suspend fun loadData(): String {
    val response = okHttpClient.newCall(request).execute() // warning here
    return response.body!!.string()
}

The IDE shows the warning:

Inappropriate blocking method call

What does this warning mean in the context of coroutines, and what is the best way to handle blocking APIs like OkHttp or Moshi correctly?

Short Answer

By the end of this page, you will understand why Kotlin warns about blocking method calls inside coroutines, when that warning matters, and how to fix it properly using the right coroutine dispatcher such as Dispatchers.IO. You will also see how this applies to common libraries like OkHttp and Moshi, plus common mistakes to avoid.

Concept

In Kotlin coroutines, not all threads are meant for blocking work.

A blocking call is a function that keeps the current thread busy until it finishes. Examples include:

  • Network requests using blocking APIs such as OkHttpClient.newCall(...).execute()
  • Reading files from disk
  • Database calls in blocking drivers
  • Heavy parsing or serialization in some situations

Coroutines are lightweight, but the code inside them still runs on real threads. If you call a blocking method on a thread that should stay free, you can reduce performance or freeze important work.

That is why the IDE warns about "inappropriate blocking method call".

Why the warning appears

The warning usually appears when you call a known blocking API from a coroutine running on a dispatcher that is not intended for blocking operations, such as:

  • Dispatchers.Default for CPU-bound work
  • Dispatchers.Main for UI work

Blocking on these dispatchers is a problem because:

  • Dispatchers.Main can freeze the UI
  • Dispatchers.Default has a limited pool meant for computation, so blocking it can starve CPU tasks

The usual fix

Move blocking work to Dispatchers.IO:

suspend fun loadData(): String = withContext(Dispatchers.IO) {
    val response = okHttpClient.newCall(request).execute()
    response.body!!.string()
}

Dispatchers.IO is designed for blocking I/O tasks like:

  • network access
  • disk access
  • database calls

What about Moshi?

Moshi itself is often used for JSON parsing. Parsing is usually CPU work, not I/O. But if your parsing is tied to reading from a blocking stream or response body, then the whole operation may belong in Dispatchers.IO.

A simple rule:

  • Blocking network/disk/stream access -> Dispatchers.IO
  • Pure CPU-heavy processing -> Dispatchers.Default

Important idea

The warning does not mean you cannot use blocking libraries in coroutines. It means you should run them on the right dispatcher.

Mental Model

Think of coroutine dispatchers like different workstations in an office:

  • Dispatchers.Main is the front desk. It must stay responsive.
  • Dispatchers.Default is the analysis team. They do thinking and calculations.
  • Dispatchers.IO is the back office for slow external tasks like waiting on files or network responses.

If you make the front desk employee wait on a package delivery for 10 minutes, nobody can greet visitors.

If you make the analysis team sit and wait on network calls, they stop doing useful computation.

So when a method blocks, send it to the back office: Dispatchers.IO.

Take Quiz

Syntax and Examples

The core tool for moving work to the correct dispatcher is withContext.

Basic syntax

suspend fun example(): Result = withContext(Dispatchers.IO) {
    // blocking code here
    blockingCall()
}

Example with OkHttp

suspend fun fetchUserJson(client: OkHttpClient, request: Request): String {
    return withContext(Dispatchers.IO) {
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw IOException("Unexpected HTTP ${response.code}")
            }
            response.body?.string() ?: throw IOException("Empty response body")
        }
    }
}

Why this works

  • execute() is a blocking network call
  • withContext(Dispatchers.IO) runs that block on a dispatcher meant for blocking I/O
  • use ensures the response is closed properly

Example with OkHttp + Moshi

data class User(val id: Int, val name: String)

suspend fun fetchUser(
    client: OkHttpClient,
    request: Request,
    adapter: com.squareup.moshi.JsonAdapter<User>
): User {
    return withContext(Dispatchers.IO) {
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw IOException("Unexpected HTTP ${response.code}")
            }

            val json = response.body?.string() ?: throw IOException("Empty response body")
            adapter.fromJson(json) ?: throw IOException("Invalid JSON")
        }
    }
}

This is reasonable because the blocking network read happens inside Dispatchers.IO.

If parsing is large and CPU-heavy

Sometimes you may separate I/O from parsing:

suspend fun fetchAndParse(
    client: OkHttpClient,
    request: Request,
    adapter: com.squareup.moshi.JsonAdapter<User>
): User {
    val json = withContext(Dispatchers.IO) {
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) throw IOException("HTTP ${response.code}")
            response.body?.string() ?: throw IOException("Empty body")
        }
    }

    return withContext(Dispatchers.Default) {
        adapter.fromJson(json) ?: throw IOException("Invalid JSON")
    }
}

This split is useful when parsing is expensive enough to matter.

Step by Step Execution

Consider this function:

suspend fun fetchTitle(client: OkHttpClient, request: Request): String {
    return withContext(Dispatchers.IO) {
        client.newCall(request).execute().use { response ->
            response.body?.string() ?: ""
        }
    }
}

Here is what happens step by step:

  1. fetchTitle() is called from a coroutine.
  2. withContext(Dispatchers.IO) suspends the current coroutine and schedules the block on an I/O thread.
  3. client.newCall(request).execute() starts a blocking HTTP request.
  4. The I/O thread waits for the server response.
  5. Once the response arrives, response.body?.string() reads the response body.
  6. use closes the response automatically.
  7. The result string is returned from the withContext block.
  8. The coroutine resumes with that string.

Why this is safe

The coroutine is still written in a sequential style, but the blocking work happens on a dispatcher built for it.

What would be risky

suspend fun fetchTitleBad(client: OkHttpClient, request: Request): String {
    return client.newCall(request).execute().body!!.string()
}

This code may run on whatever dispatcher the caller used. If that is Main or Default, the warning is justified.

Real World Use Cases

Blocking calls inside coroutines appear often in real programs.

HTTP requests

withContext(Dispatchers.IO) {
    client.newCall(request).execute()
}

Used in:

  • REST API clients
  • internal service calls
  • downloading files

File reading and writing

val text = withContext(Dispatchers.IO) {
    File("config.json").readText()
}

Used in:

  • configuration loading
  • log processing
  • import/export tools

Database access with blocking drivers

val user = withContext(Dispatchers.IO) {
    userRepository.findById(1)
}

Used in:

  • server-side applications
  • desktop apps
  • Android apps using older or blocking persistence APIs

JSON parsing after network access

Moshi often appears right after an HTTP response is read. Developers frequently keep the fetch and parse steps together, especially when the payload is small.

Legacy library integration

Coroutines are often added to an existing codebase that already uses blocking Java libraries. withContext(Dispatchers.IO) is the standard bridge.

Take Quiz

Real Codebase Usage

In real projects, developers usually do not scatter raw blocking calls everywhere. They wrap them in well-defined suspend functions.

Common pattern: repository wrapper

class UserRepository(
    private val client: OkHttpClient,
    private val adapter: com.squareup.moshi.JsonAdapter<User>
) {
    suspend fun fetchUser(request: Request): User = withContext(Dispatchers.IO) {
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw IOException("HTTP ${response.code}")
            }

            val json = response.body?.string() ?: throw IOException("Empty body")
            adapter.fromJson(json) ?: throw IOException("Invalid JSON")
        }
    }
}

This keeps blocking details in one place.

Guard clauses for validation

suspend fun fetchUser(request: Request?): User {
    requireNotNull(request) { "Request must not be null" }
    return withContext(Dispatchers.IO) {
        // blocking call
        TODO()
    }
}

Validate early before doing expensive work.

Error handling

suspend fun safeFetch(request: Request): Result<User> {
    return try {
        val user = withContext(Dispatchers.IO) {
            // blocking call and parsing
            TODO()
        }
        Result.success(user)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

Separation of concerns

Teams often separate layers like this:

  • API layer performs network I/O on Dispatchers.IO
  • parsing may stay there for small payloads or move to Dispatchers.Default if heavy
  • UI or controller layer simply calls suspend functions

Dependency-injected dispatchers

In larger codebases, dispatchers are often injected for testability:

class UserService(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
    suspend fun load(): String = withContext(ioDispatcher) {
        "data"
    }
}

This makes unit tests easier.

Common Mistakes

1. Calling blocking code directly from a suspend function

A suspend function is not automatically non-blocking.

Broken example:

suspend fun load(): String {
    return client.newCall(request).execute().body!!.string()
}

Why it is a problem:

  • suspend only means the function can suspend
  • it does not magically move work off the current thread

Fix:

suspend fun load(): String = withContext(Dispatchers.IO) {
    client.newCall(request).execute().use { response ->
        response.body?.string() ?: ""
    }
}

2. Using Dispatchers.Default for blocking I/O

Broken example:

withContext(Dispatchers.Default) {
    client.newCall(request).execute()
}

Why it is a problem:

  • Default is for CPU-bound tasks
  • blocking it can reduce throughput

Use Dispatchers.IO instead.

3. Doing blocking work on Dispatchers.Main

Broken example:

withContext(Dispatchers.Main) {
    client.newCall(request).execute()
}

This can freeze the UI.

4. Forgetting to close the response

Broken example:

val response = client.newCall(request).execute()
return response.body!!.string()

Better:

client.newCall(request).execute().use { response ->
    return response.body?.string() ?: ""
}

5. Assuming all Moshi work belongs on Dispatchers.IO

If Moshi is parsing an already loaded string and the JSON is large, that is more like CPU work.

Rule of thumb:

  • reading from network/file stream: Dispatchers.IO
  • expensive parsing/computation: Dispatchers.Default

6. Silencing the warning without fixing the cause

Do not just ignore the warning unless you know the current thread is already correct. The goal is to make the code actually safe, not only quiet.

Comparisons

SituationBest choiceWhy
Blocking network call with OkHttp execute()Dispatchers.IOIt waits on I/O
File read/writeDispatchers.IOFile access is blocking
Large JSON parsing from an in-memory stringDispatchers.DefaultIt is mainly CPU work
UI update after loading dataDispatchers.MainUI work belongs on the main thread

suspend vs withContext

ConceptWhat it means
suspend funA function that can suspend without blocking the caller thread
withContext(...)Run a block in a specific coroutine context or dispatcher

A suspend function does not automatically choose the correct dispatcher.

OkHttp execute() vs async coroutine-friendly style

ApproachTypeNotes
execute()BlockingUse inside Dispatchers.IO
callback-based enqueue()Non-blocking callback APICan be wrapped into coroutines if needed

If a library already offers a coroutine-native API, that is often nicer than wrapping a blocking call yourself. But for blocking APIs, withContext(Dispatchers.IO) is the standard approach.

Take Quiz

Cheat Sheet

withContext(Dispatchers.IO) {
    // blocking I/O work
}

Quick rules

  • suspend does not mean non-blocking by itself
  • Blocking network calls -> Dispatchers.IO
  • Blocking file/database calls -> Dispatchers.IO
  • CPU-heavy parsing/computation -> Dispatchers.Default
  • UI updates -> Dispatchers.Main

Safe OkHttp pattern

suspend fun fetch(): String = withContext(Dispatchers.IO) {
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) throw IOException("HTTP ${response.code}")
        response.body?.string() ?: throw IOException("Empty body")
    }
}

Common warning meaning

Inappropriate blocking method call usually means:

  • you are calling a blocking API
  • the current thread may not be appropriate for blocking work

Remember

  • Dispatchers.IO is the usual fix for blocking libraries
  • keep blocking code wrapped in dedicated suspend functions
  • use use {} for Response cleanup
  • do not block Main
  • do not waste Default on I/O

FAQ

Why do I get "inappropriate blocking method call" in a coroutine?

Because the method you are calling blocks the thread, and the current dispatcher may not be intended for blocking work.

Is a suspend function automatically safe for blocking code?

No. A suspend function can still block the thread if you call a blocking API directly.

Should I always use Dispatchers.IO with OkHttp?

If you are using the blocking execute() method, yes, that is the normal choice.

Should Moshi run on Dispatchers.IO or Dispatchers.Default?

It depends. If the work includes blocking stream or response reading, use IO. If it is just heavy parsing of in-memory data, Default may fit better.

Can I ignore the warning?

Only if you are certain the code is already running on a suitable thread. In most cases, it is better to fix the dispatcher explicitly.

Is Dispatchers.Default faster than Dispatchers.IO?

Not for blocking I/O. Default is tuned for CPU-bound work, while IO is designed for blocking operations.

What is the simplest correct fix?

Wrap the blocking call in withContext(Dispatchers.IO) { ... }.

Do I need to use callbacks instead of coroutines for OkHttp?

No. You can keep coroutine-style code and call blocking APIs safely on Dispatchers.IO.

Take Quiz

Mini Project

Description

Build a small suspend-based API helper that fetches JSON with OkHttp and parses it with Moshi without triggering blocking-call misuse. This demonstrates the correct way to wrap blocking library calls inside coroutines.

Goal

Create a suspend function that performs a blocking HTTP request safely on Dispatchers.IO, parses the result, and returns a Kotlin data object.

Requirements

  1. Define a simple data class to represent JSON data.
  2. Use OkHttp execute() inside a suspend function.
  3. Run the blocking network work on Dispatchers.IO.
  4. Parse the JSON response using Moshi.
  5. Throw a clear exception for HTTP errors or invalid data.
Take Quiz

Keep learning

Related questions

View more