toeverything/AFFiNE · error · ReplyNotFound

reply_not_found

reply_not_found

Error message

Reply not found.

What it means

Thrown by the `updateReply` GraphQL mutation when `service.getReply(input.id)` returns null. The resolver fetches the reply to drive `assertPermission` and the realtime publish; a missing row aborts with `reply_not_found` (category `resource_not_found`) before any write.

Source

Thrown at packages/backend/server/src/core/comment/resolver.ts:202

      ...reply,
      user: {
        id: me.id,
        name: me.name,
        avatarUrl: me.avatarUrl,
      },
    };
  }

  @Mutation(() => Boolean, {
    description: 'Update a reply content',
  })
  async updateReply(
    @CurrentUser() me: UserType,
    @Args('input') input: ReplyUpdateInput
  ) {
    const reply = await this.service.getReply(input.id);
    if (!reply) {
      throw new ReplyNotFound();
    }

    await this.assertPermission(me, reply, 'Doc.Comments.Update');

    await this.service.updateReply(input);
    publishCommentChanged(this.realtime, reply.workspaceId, reply.docId);
    return true;
  }

  @Mutation(() => Boolean, {
    description: 'Delete a reply',
  })
  async deleteReply(@CurrentUser() me: UserType, @Args('id') id: string) {
    const reply = await this.service.getReply(id);
    if (!reply) {
      throw new ReplyNotFound();
    }

View on GitHub (pinned to 26c515e050)

Solutions

  1. Catch `reply_not_found` on edit and discard the local draft with a 'reply no longer available' notice.
  2. Reconcile the edit target against the latest `comments` query before opening the editor.
  3. Subscribe to comment-changes and close the editor when the reply's deletion arrives.
  4. Validate the reply id is non-empty and locally known before mutating.

Example fix

// before
await client.mutate({
  mutation: UPDATE_REPLY,
  variables: { input: { id: replyId, content } },
});

// after
try {
  await client.mutate({
    mutation: UPDATE_REPLY,
    variables: { input: { id: replyId, content } },
  });
} catch (e) {
  if (graphQLErrorCode(e) === 'reply_not_found') {
    discardDraft(replyId);
    toast('This reply is no longer available.');
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the reply still exists in the latest comment list before editing
const { data } = await client.query({
  query: COMMENTS_QUERY,
  variables: { docId },
  fetchPolicy: 'network-only',
});
const replyStillThere = (data?.comments.edges ?? []).some(c =>
  (c.node.replies ?? []).some(r => r.id === replyId),
);
if (!replyStillThere) {
  discardDraft(replyId);
  return;
}

Type guard

function isReplyRef(value: unknown): value is { id: string } {
  return typeof value === 'object' &&
    value !== null &&
    typeof (value as { id?: unknown }).id === 'string';
}

Try / catch

try {
  await mutateUpdateReply(replyId, content);
} catch (e) {
  const code = e?.graphQLErrors?.[0]?.extensions?.code;
  if (code === 'reply_not_found') {
    discardDraft(replyId);
    toast('This reply is no longer available.');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Editing a reply that was deleted by its author or a moderator; editing a reply whose id came from a stale list; race with another client deleting the reply mid-edit.

Common situations: Editing UI left open on a removed reply; optimistic local state not reconciled with deletes; content-edit collisions where one editor removes the reply.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/88c778163e995138. Report an issue: GitHub.