{"record":{"id":"04712b4325d62676","repo":"airbnb/epoxy","slug":"failed-to-hijack-update-handler-in-asyncpagedlistdiffer-you","errorCode":null,"errorMessage":"Failed to hijack update handler in AsyncPagedListDiffer.You can only build models on the main thread","messagePattern":"Failed to hijack update handler in AsyncPagedListDiffer\\.You can only build models on the main thread","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"critical","filePath":"epoxy-paging3/src/main/java/com/airbnb/epoxy/paging3/PagedListModelCache.kt","lineNumber":169,"sourceCode":"        init {\n            if (modelBuildingHandler != EpoxyController.defaultModelBuildingHandler) {\n                try {\n                    // looks like AsyncPagedListDiffer in 1.x ignores the config.\n                    // Reflection to the rescue.\n                    val mainThreadExecutorField =\n                        AsyncPagedListDiffer::class.java.getDeclaredField(\"mainThreadExecutor\")\n                    mainThreadExecutorField.isAccessible = true\n                    mainThreadExecutorField.set(\n                        this,\n                        Executor {\n                            modelBuildingHandler.post(it)\n                        }\n                    )\n                } catch (t: Throwable) {\n                    val msg = \"Failed to hijack update handler in AsyncPagedListDiffer.\" +\n                        \"You can only build models on the main thread\"\n                    Log.e(\"PagedListModelCache\", msg, t)\n                    throw IllegalStateException(msg, t)\n                }\n            }\n        }\n    }\n\n    @Synchronized\n    fun submitList(pagedList: PagedList<T>?) {\n        inSubmitList = true\n        asyncDiffer.submitList(pagedList)\n        inSubmitList = false\n    }\n\n    @Synchronized\n    fun getModels(): List<EpoxyModel<*>> {\n        val currentList = asyncDiffer.currentList ?: emptyList<T>()\n\n        // The first time models are built the EpoxyController does so synchronously, so that\n        // the UI can be ready immediately. To avoid concurrent modification issues with the PagedList","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/airbnb/epoxy/blob/e45bd3a61fe3a1f130e184f5b8dcf172ab99025a/epoxy-paging3/src/main/java/com/airbnb/epoxy/paging3/PagedListModelCache.kt#L151-L187","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Create PagedListEpoxyController / PagedListModelCache on the main thread (e.g. in the ViewModel's main-scope or Activity).","Move any controller construction out of background coroutines: use Dispatchers.Main or withContext(Main) before instantiating.","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.","Update epoxy-paging3 to the latest version so it matches your paging-runtime version."],"exampleFix":"// before\nviewModelScope.launch(Dispatchers.IO) {\n  val controller = MyPagedListController() // throws\n}\n\n// after\nviewModelScope.launch(Dispatchers.Main) {\n  val controller = MyPagedListController()\n}","handlingStrategy":"try-catch","validationCode":"fun ensureMainThread() {\n  check(Looper.myLooper() == Looper.getMainLooper()) {\n    \"PagedListEpoxyController must be created on the main thread\"\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  val controller = MyPagedListController()\n} catch (e: IllegalStateException) {\n  if (e.message?.contains(\"hijack update handler\") == true) {\n    Log.e(TAG, \"Controller built off main thread or paging internals changed\", e)\n  } else throw e\n}","preventionTips":["Instantiate paging controllers on the main thread only (ViewModel main scope, onCreate).","Never construct Epoxy controllers inside Dispatchers.IO/Default coroutines.","Keep epoxy-paging3 and androidx paging versions in sync; check the wrapped cause on upgrades."],"tags":["android","epoxy","paging","main-thread","reflection"],"backgroundTag":"thread-interrupted","analyzedSha":"e45bd3a61fe3a1f130e184f5b8dcf172ab99025a","analyzedAt":"2026-09-13T03:24:16.052Z","contentChangedAt":"2026-09-13T03:24:16.052Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}