block/buzz · error · ThreadExpectedEventMissingError

Thread fetch completed but expected event ${expectedEventId}

Error message

Thread fetch completed but expected event ${expectedEventId} is absent.

What it means

loadThreadReplies paginates thread replies and validates the result: if the expected (originally clicked) event id is not in the fetched page set and it wasn't already covered by an exhausted target, it throws ThreadExpectedEventMissingError instead of rendering a thread that lost its anchor message. A separate plain Error guards the page safety limit.

Source

Thrown at desktop/src/features/messages/useThreadReplies.ts:83

        (event) => !idsAtStart.has(event.id),
      );
      const result = sortMessages([...replies, ...receivedInFlight]);
      // When the caller expects a specific reply event (e.g. opened from a
      // notification) and the completed fetch does not contain it, the relay
      // likely delivered an empty result before the event was replicated. Throw
      // so React Query's retry-with-backoff re-attempts instead of caching an
      // authoritative empty — heals automatically once the relay catches up.
      //
      // Once the target has been declared permanently absent (exhaustedTargets
      // contains it), stop throwing: the successfully fetched replies are
      // rendered and the unreachable target is quietly retired rather than
      // painting the whole thread as a terminal load error.
      if (
        expectedEventId &&
        !result.some((e) => e.id === expectedEventId) &&
        !exhaustedTargets?.has(expectedEventId)
      ) {
        throw new ThreadExpectedEventMissingError(expectedEventId);
      }
      return result;
    }
    cursor = response.nextCursor;
  }
  throw new Error(`Thread ${rootId} exceeded the page safety limit.`);
}

/** Fetch a thread subtree into a cache independent from channel window pages. */
export function useThreadReplies(
  activeChannel: Channel | null,
  openThreadRootId: string | null,
  expectedEventId?: string | null,
) {
  const channelId = activeChannel?.id ?? "none";
  const rootId = openThreadRootId ?? "none";
  const queryClient = useQueryClient();
  const queryKey = threadRepliesKey(channelId, rootId);

View on GitHub (pinned to dad5a33865)

Solutions

  1. Refresh the channel list and reopen the thread — if the anchor was deleted, treat the thread as unavailable rather than retrying.
  2. Check relay connectivity and re-run the REQ; a partial fetch can miss the expected event.
  3. If the page safety limit fired, raise the limit or fetch the thread with narrower filters/time bounds.
  4. Verify the event still exists via a direct relay query by id before rendering the thread.

Example fix

// before
const replies = await loadThreadReplies(rootId, expectedEventId);
renderThread(replies);

// after
try {
  const replies = await loadThreadReplies(rootId, expectedEventId);
  renderThread(replies);
} catch (e) {
  if (e instanceof ThreadExpectedEventMissingError) {
    showThreadUnavailable(e.expectedEventId); // 'message was removed' state
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const anchor = await fetchEventById(expectedEventId); // verify it still exists before opening the thread
if (!anchor) showThreadUnavailable();

Type guard

import { ThreadExpectedEventMissingError } from "@/features/messages/useThreadReplies";
function isExpectedEventMissing(e: unknown): e is ThreadExpectedEventMissingError {
  return e instanceof ThreadExpectedEventMissingError;
}

Try / catch

try {
  const replies = await loadThreadReplies(rootId, expectedEventId);
} catch (e) {
  if (isExpectedEventMissing(e)) {
    renderThreadUnavailableNotice(e.expectedEventId);
    return;
  }
  if (e instanceof Error && e.message.includes("page safety limit")) {
    renderPartialThreadWithLoadMore();
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Opening a thread whose root/anchor event was deleted or not yet replicated, so a completed fetch returns pages lacking expectedEventId while exhaustedTargets doesn't cover it; also triggered when the thread exceeds the max-page safety limit and the loop exits without the expected event.

Common situations: Message was deleted on the relay between the channel view and thread open; relay outage/gap caused a partial history fetch; very long thread hitting the page safety limit; stale deep-link to a removed event.

Related errors


AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05). Data as JSON: /api/errors/532450b6800a0fd8. Report an issue: GitHub.