Why Room complains about the main thread? Analyzing the error and 3 ways to fix it

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: why a "simple SELECT" turns into a headache

\n\n

You are writing your first Android app with Room and Kotlin. Everything goes smoothly until you need to read data from the database. You write something like:

\n\n
val user = userDao.getUserById(42)\ntextView.text = user.name
\n\n

You run it — and the app crashes with an exception:

\n\n
java.lang.IllegalStateException: Cannot access database on the main thread\nsince it may potentially lock the UI for a long period of time.
\n\n

Familiar? This question on Stack Overflow has gathered thousands of views and hundreds of comments. The error scares beginners, but in reality it's not a bug — it's foolproof protection. Room (like any competent ORM) prevents you from accidentally blocking the user interface.

\n\n

In this article, we will analyze why this happens and how to properly execute SELECT queries in Android Room with Kotlin. You will learn three proven approaches: LiveData, Coroutines, and RxJava. Let's go!

\n\n

The gist: why does Room block the main thread?

\n\n

Android is a single-threaded system by default. All UI runs on the main thread. If you start reading the database on this thread, the interface will freeze until the query completes. With large amounts of data, this can take seconds — the user will see a "hanging" app.

\n\n

Room plays it safe: it forbids any long operations (reading, writing) on the main thread. The exception is if you explicitly allow this via .allowMainThreadQueries() when building the database. But you shouldn't do this — it's an anti-pattern.

\n\n

Here's what a typical problem looks like:

\n\n
// DAO\n@Dao\ninterface UserDao {\n    @Query("SELECT * FROM users WHERE id = :userId")\n    suspend fun getUserById(userId: Int): User  // suspend — already a hint!\n}\n\n// Activity\nclass MainActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        // ❌ Error!\n        val user = database.userDao().getUserById(42)\n    }\n}
\n\n

Even if you made the function suspend, but call it without a coroutine — Room will still throw an exception. Let's fix it!

\n\n

Approach 1: LiveData — simple and reactive

\n\n

The simplest way is to return LiveData from the DAO. Room manages the thread itself: the query runs in the background, and the result arrives in the UI.

\n\n

Step 1. Modify the DAO

\n\n
@Dao\ninterface UserDao {\n    @Query("SELECT * FROM users WHERE id = :userId")\n    fun getUserById(userId: Int): LiveData<User>\n}
\n\n

Step 2. Subscribe in Activity/Fragment

\n\n
class MainActivity : AppCompatActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        database.userDao().getUserById(42).observe(this) { user ->\n            textView.text = user.name\n        }\n    }\n}
\n\n

Pros: minimal code, automatic unsubscription when the Activity is destroyed, reactivity — data updates automatically when the table changes.

\n\n

Cons: LiveData is tied to the lifecycle, it's not easy to combine with other data sources (network, SharedPreferences).

\n\n

Approach 2: Coroutines — the modern Kotlin way

\n\n

If you are using Kotlin Coroutines (and you should), this is the most elegant way. The function is marked as suspend, and the call is wrapped in a coroutine with the correct Dispatchers.IO.

\n\n

Step 1. DAO with suspend

\n\n
@Dao\ninterface UserDao {\n    @Query("SELECT * FROM users WHERE id = :userId")\n    suspend fun getUserById(userId: Int): User\n}
\n\n

Step 2. Call from ViewModel (recommended)

\n\n
class MainVi                

Blogs

Book Recommendations