DrKLO/Telegram · critical · IllegalStateException

DiffUtil hit an unexpected case while trying to calculate th

Error message

DiffUtil hit an unexpected case while trying to calculate the optimal path. Please make sure your data is not changing during the diff calculation.

What it means

DiffUtil computes a minimal edit script between two lists using Myers' snake algorithm with forward and backward iteration over a diagonal k-axis. The algorithm assumes it always finds an overlapping snake (forward[k] >= backward[k]) within the loop bounds for a well-formed input. The 'unexpected case' means the overlapping condition was never met — which is mathematically impossible for immutable inputs of equal total size, so it implies the list sizes or contents changed DURING the diff. This is the canonical signature of a non-stable DiffUtil.Callback whose areItemsTheSame/getOldListSize return different values across calls.

Source

Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/DiffUtil.java:287

                    x--;
                    y--;
                }
                backward[kOffset + backwardK] = x;
                if (!checkInFwd && k + delta >= -d && k + delta <= d) {
                    if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
                        Snake outSnake = new Snake();
                        outSnake.x = backward[kOffset + backwardK];
                        outSnake.y = outSnake.x - backwardK;
                        outSnake.size =
                                forward[kOffset + backwardK] - backward[kOffset + backwardK];
                        outSnake.removal = removal;
                        outSnake.reverse = true;
                        return outSnake;
                    }
                }
            }
        }
        throw new IllegalStateException("DiffUtil hit an unexpected case while trying to calculate"
                + " the optimal path. Please make sure your data is not changing during the"
                + " diff calculation.");
    }

    /**
     * A Callback class used by DiffUtil while calculating the diff between two lists.
     */
    public abstract static class Callback {
        /**
         * Returns the size of the old list.
         *
         * @return The size of the old list.
         */
        public abstract int getOldListSize();

        /**
         * Returns the size of the new list.
         *

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Pass a defensive copy (new ArrayList<>(currentList)) to the DiffUtil.Callback so its size and contents are frozen for the duration.
  2. Run DiffUtil.calculateDiff on a background thread and only dispatchUpdatesTo on the main thread.
  3. Serialize list mutations: queue incoming items and apply them between diffs, never during.
  4. If using ListAdapter (PagedList/AsyncListDiffer), ensure submitList is used rather than mutating the backing list directly.

Example fix

// before
List<Item> live = adapter.getItems(); // mutating during diff
DiffUtil.DiffResult r = DiffUtil.calculateDiff(new DiffUtil.Callback() {
    public int getOldListSize() { return live.size(); } // changes mid-run!
    ...
});

// after
List<Item> snapshot = new ArrayList<>(live);
DiffUtil.DiffResult r = DiffUtil.calculateDiff(new DiffUtil.Callback() {
    public int getOldListSize() { return snapshot.size(); }
    ...
});
Defensive patterns

Strategy: validation

Validate before calling

// Snapshot lists before diffing to freeze size and contents
List<Item> oldSnap = new ArrayList<>(current);
List<Item> newSnap = new ArrayList<>(next);
DiffUtil.DiffResult r = DiffUtil.calculateDiff(new DiffUtil.Callback() {
    public int getOldListSize() { return oldSnap.size(); }
    public int getNewListSize() { return newSnap.size(); }
    public boolean areItemsTheSame(int o, int n) { return oldSnap.get(o).id == newSnap.get(n).id; }
    public boolean areContentsTheSame(int o, int n) { return oldSnap.get(o).equals(newSnap.get(n)); }
});

Prevention

When it happens

Trigger: The backing list mutated while DiffUtil.calculateDiff was running (e.g. items added/removed on the main thread during a background diff); a DiffUtil.Callback that reads a live ArrayList/ConcurrentHashMap whose size shifts between getOldListSize and areItemsTheSame calls; calling adapter.setNewList while a previous diff is still computing.

Common situations: Using a plain mutable List as the source for both the adapter and the DiffUtil.Callback without snapshotting; paging/infinite-scroll that appends items mid-diff; a chat-style adapter (Telegram) where new messages arrive during a DiffUtil refresh.

Related errors


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