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
- Refresh the channel list and reopen the thread — if the anchor was deleted, treat the thread as unavailable rather than retrying.
- Check relay connectivity and re-run the REQ; a partial fetch can miss the expected event.
- If the page safety limit fired, raise the limit or fetch the thread with narrower filters/time bounds.
- 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
- Verify the anchor event exists via direct id query when opening from deep links.
- Treat exhausted-targets metadata as part of the fetch contract; always pass it through.
- Keep page limits generous but bounded; offer explicit 'load more' instead of failing.
- Refresh channel data on reconnect so deleted anchors are reflected before thread open.
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
- Could not load active huddles: pagination cursor did not adv
- an admin cannot ban or time out a community owner or fellow
- BUZZ_RELAY_PRIVATE_KEY must be set when BUZZ_REQUIRE_AUTH_TO
- failed to sign member snapshot: {error}
- repo announcement rejected by community deletion fence: {e}
AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05).
Data as JSON: /api/errors/532450b6800a0fd8.
Report an issue: GitHub.