DrKLO/Telegram · critical · RuntimeException

Holder at{i} {holder} ...

Error message

Holder at{i} {holder}
...

What it means

In dispatchLayoutStep3 (the animation phase of layout), RecyclerView wraps the entire change-matching loop in try/catch. If ANY exception is thrown while recording post-layout info or matching changed ViewHolders, it rethrows a RuntimeException whose message is a dump of every attached, non-ignored ViewHolder with its index. This is a diagnostic wrapper: the real failure is the chained cause (Throwable e), not the holder list itself. The list is provided so the developer can inspect the live view-holder state at the moment of failure.

Source

Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java:4210

                            } else {
                                animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                        oldDisappearing, newDisappearing);
                            }
                        }
                    } else {
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    }
                }
            } catch (Exception e) {
                StringBuilder builder = new StringBuilder();
                for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                    ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                    if (holder == null || holder.shouldIgnore()) {
                        continue;
                    }
                    builder.append("Holder at" + i + " " + holder + "\n");
                }
                throw new RuntimeException(builder.toString(), e);
            }

            // Step 4: Process view info lists and trigger animations
            mViewInfoStore.process(mViewInfoProcessCallback);
        }

        mLayout.removeAndRecycleScrapInt(mRecycler);
        mState.mPreviousLayoutItemCount = mState.mItemCount;
        mDataSetHasChangedAfterLayout = false;
        mDispatchItemsChangedEvent = false;
        mState.mRunSimpleAnimations = false;

        mState.mRunPredictiveAnimations = false;
        mLayout.mRequestedSimpleAnimations = false;
        if (mRecycler.mChangedScrap != null) {
            mRecycler.mChangedScrap.clear();
        }
        if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Inspect the chained cause (RuntimeException.getCause()) — that is the real failure; the holder dump is only context. Fix the underlying defect the cause names (duplicate stable ID, invalid position, NPE in custom animator).
  2. If the cause points to a custom ItemAnimator, audit its recordPostLayoutInformation/animateChange implementations for null checks and for touching holders that may have been recycled.
  3. Ensure all adapter mutations happen on the main thread and are never concurrent with a layout pass; route mutations through a single-threaded queue or DiffUtil.
  4. Temporarily disable item animations (recyclerView.setItemAnimator(null)) to confirm whether the crash is animation-path-only; if it disappears, the ItemAnimator or an inconsistent-change notification is the culprit.

Example fix

// before: a custom ItemAnimator throwing during change animation
@Override public boolean animateChange(@NonNull RecyclerView.ViewHolder oldVh,
    @NonNull RecyclerView.ViewHolder newVh,
    @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
  return animateMove(newVh, preInfo.left, preInfo.top, postInfo.left, postInfo.top); // NPE if preInfo null
}
// after: guard against null info and fall back to a no-op change
@Override public boolean animateChange(@NonNull RecyclerView.ViewHolder oldVh,
    @NonNull RecyclerView.ViewHolder newVh,
    @Nullable ItemHolderInfo preInfo, @Nullable ItemHolderInfo postInfo) {
  if (preInfo == null || postInfo == null) {
    dispatchChangeFinished(newVh, false);
    return false;
  }
  return animateMove(newVh, preInfo.left, preInfo.top, postInfo.left, postInfo.top);
}
Defensive patterns

Strategy: try-catch

Try / catch

// This is a framework-thrown wrapper; catch only at a top-level uncaught-exception handler
// to report diagnostics, then FIX the chained cause rather than suppress it.
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
  if (e instanceof RuntimeException && e.getMessage() != null
      && e.getMessage().startsWith("Holder at")) {
    reportToCrashAnalytics(e.getCause() != null ? e.getCause() : e);
  }
});

Prevention

When it happens

Trigger: An exception thrown inside ItemAnimator.recordPostLayoutInformation, animateChange, or animateMove/Remove during Step 3; or a downstream failure in mViewInfoStore operations. Typically caused by a custom ItemAnimator that throws, by the same duplicate-key/invalid-position defects (see errors 42/43/49) surfacing during animation, or by a ViewHolder being modified concurrently.

Common situations: Custom ItemAnimator with a null/buggy implementation; concurrent adapter mutation from a background thread that invalidates holders mid-animation; an ItemAnimator touching a recycled view; a prior inconsistent notify sequence that only surfaces corruption during Step 3.

Related errors


AI-assisted analysis of DrKLO/Telegram@45ab8f4308 (2026-08-14). Data as JSON: /api/errors/0c6daea05903469d. Report an issue: GitHub.