Kotlin

Android Room - simple select query - Cannot access database on the main thread

25 September 2026 · 12 min read

Android Room - simple select query - Cannot access database on the main thread

Developing Android applications often involves working with local databases, and the Android Room Persistence Library simplifies this process significantly. However, developers frequently encounter the dreaded “Cannot access database on the main thread” error when performing a simple select query using Room. This error arises because database operations can be time-consuming, and performing them on the main thread can lead to application freezes and a poor user experience. Understanding asynchronous programming and proper threading techniques is crucial to resolving this issue and ensuring a smooth, responsive application. This article dives deep into the causes of this common problem and provides practical solutions and best practices for handling database queries effectively with Android Room, ensuring your app remains performant and user-friendly.

Understanding the “Cannot access database on the main thread” Error

The “Cannot access database on the main thread” exception is Android’s way of preventing you from blocking the UI. The main thread, also known as the UI thread, is responsible for handling user interactions and updating the screen. If a long-running operation, such as a database query, is executed on this thread, it can cause the UI to become unresponsive, leading to a frustrating user experience. Android enforces this restriction to ensure application responsiveness and prevent Application Not Responding (ANR) errors. Room, by default, inherits this behavior to promote good coding practices and prevent accidental blocking of the main thread, even when performing a simple select query.

The root cause lies in the architecture of Android applications and the single-threaded nature of the UI. All UI updates and event handling occur on the main thread. Any operation that takes a significant amount of time (typically more than a few milliseconds) should be offloaded to a background thread. Database operations, especially those involving large datasets or complex queries, can easily exceed this threshold. Failing to do so results in the “Cannot access database on the main thread” error. To avoid this, Room requires you to explicitly handle database operations asynchronously using techniques like coroutines, RxJava, or LiveData. Using these tools ensures that your UI remains responsive, while the database query executes in the background.

Consider a scenario where you’re building an app that displays a list of products fetched from a local database. If you perform the database query to retrieve the product list directly on the main thread, the UI will freeze until the query completes. This means the user won’t be able to scroll, tap buttons, or interact with the app in any way during this time. This is a terrible user experience and can lead users to abandon your app. Asynchronous operations keep the UI fluid and responsive, even while fetching data. The key is to move the database query off the main thread and update the UI once the data is retrieved. This approach prevents blocking and ensures a smooth and interactive application.

Asynchronous Solutions with Android Room

Android Room provides several mechanisms for performing database operations asynchronously. The most common approaches involve using Kotlin Coroutines, RxJava, or LiveData. Each method offers different advantages and trade-offs, so choosing the right approach depends on the specific requirements of your application. Kotlin Coroutines are a popular choice due to their simplicity and conciseness. RxJava offers more advanced features for handling asynchronous data streams, while LiveData integrates seamlessly with the Android lifecycle and is ideal for observing changes in the database.

One common solution involves using Kotlin Coroutines. Coroutines allow you to write asynchronous code in a sequential, easy-to-read manner. You can define your database queries as suspend functions, which can be executed in a background thread without blocking the main thread. Here’s an example of how you might implement a simple select query using Kotlin Coroutines:

  1. Wrap your database query function with the suspend keyword.
  2. Use a CoroutineScope (e.g., viewModelScope in a ViewModel) to launch the coroutine.
  3. Call the suspend function within the coroutine scope using Dispatchers.IO to execute the query on a background thread.
  4. Update the UI with the results on the Dispatchers.Main thread.

This approach ensures that the database query is executed in the background and that the UI is updated safely on the main thread. Another powerful approach is to use RxJava. RxJava allows you to work with asynchronous data streams using Observables. You can wrap your database queries in an Observable and subscribe to it on a background thread. When the query completes, you can then update the UI with the results on the main thread. RxJava offers a rich set of operators for transforming and filtering data streams, making it a powerful tool for complex asynchronous operations. However, it also has a steeper learning curve than Coroutines. If you want to delve into RxJava, check out resources like ReactiveX documentation [^1^][https://reactivex.io/documentation/observable.html].

Implementing Asynchronous Queries with Kotlin Coroutines: A Detailed Example

To illustrate how to implement asynchronous queries using Kotlin Coroutines, let’s consider a practical example. Suppose you have a User entity and a UserDao interface with a getUserById function that retrieves a user from the database by their ID. To perform this query asynchronously, you can wrap the getUserById function with the suspend keyword and then launch a coroutine to execute the query on a background thread. This ensures that the UI remains responsive while the query is executed.

First, let’s define our User entity and UserDao interface: kotlin @Entity(tableName = “users”) data class User( @PrimaryKey val id: Int, val name: String, val email: String ) @Dao interface UserDao { @Query(“SELECT FROM users WHERE id = :id”) suspend fun getUserById(id: Int): User? } Next, we’ll create a ViewModel that uses a CoroutineScope to launch the database query: kotlin class UserViewModel(private val userDao: UserDao) : ViewModel() { private val _user = MutableLiveData() val user: LiveData = _user fun fetchUser(userId: Int) { viewModelScope.launch(Dispatchers.IO) { val user = userDao.getUserById(userId) withContext(Dispatchers.Main) { _user.value = user } } } } In this example, the fetchUser function launches a coroutine using viewModelScope. The Dispatchers.IO context specifies that the coroutine should be executed on a background thread. Once the query completes, the withContext(Dispatchers.Main) block ensures that the UI is updated on the main thread. This approach provides a clear and concise way to perform asynchronous database queries without blocking the UI. Using proper dependency injection for the UserDao is recommended for testability and maintainability. Also, be sure to check out the official Android documentation for more details [^2^][https://developer.android.com/kotlin/coroutines].

This approach is highly recommended for simple select queries. The key is to leverage the power of coroutines to handle the asynchronous execution and the Dispatchers to manage the threading. Remember to handle potential exceptions and errors appropriately to ensure the robustness of your application. Regularly check the Android documentation for updates and best practices in using Coroutines with Room. You should also familiarize yourself with the concept of LiveData, which is often used in conjunction with Room and Coroutines to observe changes in the database and update the UI accordingly. The combination of these technologies enables you to build responsive and data-driven Android applications.

Best Practices and Common Pitfalls

While using asynchronous techniques resolves the “Cannot access database on the main thread” error, it’s important to follow best practices to ensure efficient and maintainable code. One common pitfall is neglecting to handle exceptions properly. Database operations can fail for various reasons, such as network connectivity issues or data corruption. Therefore, it’s crucial to wrap your database queries in try-catch blocks and handle any exceptions that may occur. This prevents your application from crashing and provides a more robust user experience.

Another best practice is to avoid performing complex data transformations on the main thread. While offloading the database query to a background thread solves the immediate problem, performing extensive data processing on the main thread can still lead to UI lag. Instead, perform data transformations on a background thread before updating the UI. This ensures that the UI remains responsive and that the application performs smoothly. Furthermore, always close database connections and cursors properly to prevent memory leaks and resource exhaustion. Room manages these resources automatically in most cases, but it’s still important to be aware of the underlying mechanisms and potential issues.

Here are a couple of key points to keep in mind:

  • Always use asynchronous operations for database queries.
  • Handle exceptions and errors gracefully.

And here are some common pitfalls to avoid:

  • Performing complex data transformations on the main thread.
  • Neglecting to close database connections and cursors.

Finally, consider using a dependency injection framework like Dagger or Hilt to manage your Room database and Dao instances. This promotes loose coupling, improves testability, and simplifies the management of dependencies in your application. Dependency injection allows you to easily swap out different implementations of your database and Dao for testing purposes, making it easier to write unit tests and ensure the correctness of your code. By following these best practices, you can ensure that your Android Room database operations are efficient, reliable, and maintainable. You can consult the official Google documentation on best practices to improve your apps [^3^][https://developer.android.com/topic/performance/guidelines].

Featured snippet:

The “Cannot access database on the main thread” error in Android Room occurs because database operations can be time-consuming, potentially blocking the UI thread and causing unresponsiveness. To fix this, always perform database queries asynchronously using Kotlin Coroutines, RxJava, or LiveData. These techniques ensure that database operations are executed on a background thread, keeping the UI responsive and preventing Application Not Responding (ANR) errors. Remember to handle exceptions and update the UI on the main thread after the query completes for a seamless user experience.

Infographic here
FAQ: Addressing Common Concerns -------------------------------
Why is the "Cannot access database on the main thread" error so common?
This error is common because developers sometimes forget that database operations can be slow and should not be performed on the main thread. The main thread is responsible for handling UI updates, and blocking it leads to a poor user experience. Room enforces this restriction to encourage developers to use asynchronous techniques.
Can I disable the main thread check in Room?
While it's technically possible to disable the main thread check in Room using allowMainThreadQueries(), it's strongly discouraged. Disabling this check can lead to UI freezes and ANR errors, especially for complex queries. It's always better to use asynchronous techniques to perform database operations.
How do I choose between Coroutines, RxJava, and LiveData for asynchronous queries?
The choice depends on your specific needs and preferences. Coroutines are a good choice for simple asynchronous operations due to their simplicity and conciseness. RxJava is more powerful for complex data streams but has a steeper learning curve. LiveData is ideal for observing changes in the database and integrating with the Android lifecycle.
What happens if I ignore the "Cannot access database on the main thread" error?
If you ignore this error, your application will likely become unresponsive and may eventually crash. Users will experience UI freezes and may be unable to interact with the app. This can lead to a negative user experience and potentially cause users to abandon your app.
By understanding the root cause of the "Cannot access database on the main thread" error and implementing asynchronous solutions, you can ensure that your Android applications are responsive, reliable, and provide a great user experience. Remember to follow best practices, handle exceptions properly, and choose the right asynchronous technique for your specific needs. [Learn more about optimizing Android apps.](https://courthousezoological.com/n7sqp6kh?key=e6dd02bc5dbf461b97a9da08df84d31c) We've covered the importance of asynchronous database queries using Android Room, focusing on how to avoid the "Cannot access database on the main thread" error. We explored solutions using Kotlin Coroutines, RxJava, and LiveData, highlighting their respective strengths and weaknesses. By implementing these techniques and following the best practices outlined in this article, you can build robust, responsive, and user-friendly Android applications. Don't let database operations slow down your app; embrace asynchronous programming and unlock the full potential of Android Room. If you found this helpful, share it with your fellow developers and explore related topics like advanced Room queries, database migrations, and data synchronization. **Question & Answer :** I am trying a sample with [Room Persistence Library](https://developer.android.com/topic/libraries/architecture/room.html). I created an Entity:
@Entity public class Agent { @PrimaryKey public String guid; public String name; public String email; public String password; public String phone; public String licence; } 

Created a DAO class:

@Dao public interface AgentDao { @Query("SELECT COUNT(*) FROM Agent where email = :email OR phone = :phone OR licence = :licence") int agentsCount(String email, String phone, String licence); @Insert void insertAgent(Agent agent); } 

Created the Database class:

@Database(entities = {Agent.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract AgentDao agentDao(); } 

Exposed database using below subclass in Kotlin:

class MyApp : Application() { companion object DatabaseSetup { var database: AppDatabase? = null } override fun onCreate() { super.onCreate() MyApp.database = Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").build() } } 

Implemented below function in my activity:

void signUpAction(View view) { String email = editTextEmail.getText().toString(); String phone = editTextPhone.getText().toString(); String license = editTextLicence.getText().toString(); AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao(); //1: Check if agent already exists int agentsCount = agentDao.agentsCount(email, phone, license); if (agentsCount > 0) { //2: If it already exists then prompt user Toast.makeText(this, "Agent already exists!", Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show(); onBackPressed(); } } 

Unfortunately on execution of above method it crashes with below stack trace:

FATAL EXCEPTION: main Process: com.example.me.MyApp, PID: 31592 java.lang.IllegalStateException: Could not execute method for android:onClick at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293) at android.view.View.performClick(View.java:5612) at android.view.View$PerformClick.run(View.java:22288) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6123) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Method.invoke(Native Method) at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) at android.view.View.performClick(View.java:5612)  at android.view.View$PerformClick.run(View.java:22288)  at android.os.Handler.handleCallback(Handler.java:751)  at android.os.Handler.dispatchMessage(Handler.java:95)  at android.os.Looper.loop(Looper.java:154)  at android.app.ActivityThread.main(ActivityThread.java:6123)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)  Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time. at android.arch.persistence.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:137) at android.arch.persistence.room.RoomDatabase.query(RoomDatabase.java:165) at com.example.me.MyApp.RoomDb.Dao.AgentDao_Impl.agentsCount(AgentDao_Impl.java:94) at com.example.me.MyApp.View.SignUpActivity.signUpAction(SignUpActivity.java:58) at java.lang.reflect.Method.invoke(Native Method)  at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)  at android.view.View.performClick(View.java:5612)  at android.view.View$PerformClick.run(View.java:22288)  at android.os.Handler.handleCallback(Handler.java:751)  at android.os.Handler.dispatchMessage(Handler.java:95)  at android.os.Looper.loop(Looper.java:154)  at android.app.ActivityThread.main(ActivityThread.java:6123)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)  

Seems like that problem is related to execution of db operation on main thread. However the sample test code provided in above link does not run on a separate thread:

@Test public void writeUserAndReadInList() throws Exception { User user = TestUtil.createUser(3); user.setName("george"); mUserDao.insert(user); List<User> byName = mUserDao.findUsersByName("george"); assertThat(byName.get(0), equalTo(user)); } 

Am I missing anything over here? How can I make it execute without crash? Please suggest.

It’s not recommended but you can access to database on main thread with allowMainThreadQueries()

MyApp.database = Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()