airbnb/epoxy · critical · IllegalStateException

Failed to hijack update handler in AsyncPagedListDiffer.You…

Error message

Failed to hijack update handler in AsyncPagedListDiffer.You can only build models on the main thread

What it means

PagedListModelCache hooks into Paging 2/3's AsyncPagedListDiffer by replacing its internal update Handler via reflection so list updates are dispatched on the main thread where models are built. If the reflection hijack fails, the cache cannot guarantee main-thread delivery, so it logs and rethrows this IllegalStateException wrapping the original cause.

Solutions

  1. Create PagedListEpoxyController / PagedListModelCache on the main thread (e.g. in the ViewModel's main-scope or Activity).
  2. Move any controller construction out of background coroutines: use Dispatchers.Main or withContext(Main) before instantiating.
  3. Inspect the wrapped cause (t) in the message: if it is a reflection error (NoSuchFieldException), the Paging version's internals changed — pin a compatible paging library version or upgrade Epoxy.
  4. Update epoxy-paging3 to the latest version so it matches your paging-runtime version.

Example fix

// before
viewModelScope.launch(Dispatchers.IO) {
  val controller = MyPagedListController() // throws
}

// after
viewModelScope.launch(Dispatchers.Main) {
  val controller = MyPagedListController()
}
Defensive patterns

Strategy: try-catch

Validate before calling

fun ensureMainThread() {
  check(Looper.myLooper() == Looper.getMainLooper()) {
    "PagedListEpoxyController must be created on the main thread"
  }
}

Try / catch

try {
  val controller = MyPagedListController()
} catch (e: IllegalStateException) {
  if (e.message?.contains("hijack update handler") == true) {
    Log.e(TAG, "Controller built off main thread or paging internals changed", e)
  } else throw e
}

Prevention

When it happens

Trigger: Constructing PagedListModelCache (via PagedListEpoxyController) on a background thread, so the differ's update handler/main looper assumptions do not hold and the hijack reflection fails.

Common situations: Building the Epoxy controller or submitting paged lists from a coroutine on Dispatchers.IO/Default; initializing paging in a ViewModel on a worker thread; library upgrades where AsyncPagedListDiffer internals changed and reflection can no longer find the field.

Related errors


AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13). Data as JSON: /api/errors/04712b4325d62676. Report an issue: GitHub.

Appendix: source

Thrown at epoxy-paging3/src/main/java/com/airbnb/epoxy/paging3/PagedListModelCache.kt:169

        init {
            if (modelBuildingHandler != EpoxyController.defaultModelBuildingHandler) {
                try {
                    // looks like AsyncPagedListDiffer in 1.x ignores the config.
                    // Reflection to the rescue.
                    val mainThreadExecutorField =
                        AsyncPagedListDiffer::class.java.getDeclaredField("mainThreadExecutor")
                    mainThreadExecutorField.isAccessible = true
                    mainThreadExecutorField.set(
                        this,
                        Executor {
                            modelBuildingHandler.post(it)
                        }
                    )
                } catch (t: Throwable) {
                    val msg = "Failed to hijack update handler in AsyncPagedListDiffer." +
                        "You can only build models on the main thread"
                    Log.e("PagedListModelCache", msg, t)
                    throw IllegalStateException(msg, t)
                }
            }
        }
    }

    @Synchronized
    fun submitList(pagedList: PagedList<T>?) {
        inSubmitList = true
        asyncDiffer.submitList(pagedList)
        inSubmitList = false
    }

    @Synchronized
    fun getModels(): List<EpoxyModel<*>> {
        val currentList = asyncDiffer.currentList ?: emptyList<T>()

        // The first time models are built the EpoxyController does so synchronously, so that
        // the UI can be ready immediately. To avoid concurrent modification issues with the PagedList

View on GitHub (pinned to e45bd3a61f)