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
- 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.
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
- 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.
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
- Unable to invoke
- Unable to get Epoxy helper class.
- Unable to find Epoxy Helper constructor for
- Room couldn't find the constructor it is looking for in…
- Must have stable ids when saving view holder state
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 PagedListView on GitHub (pinned to e45bd3a61f)