toeverything/AFFiNE · warning · NotificationNotFound

notification_not_found

notification_not_found

Error message

Notification not found.

What it means

NotificationService.markAsRead calls models.notification.markAsRead(notificationId, userId) and translates Prisma error P2025 ('An operation failed because it depends on one or more records that were required but not found' — here, the update matched no row) into NotificationNotFound (notification_not_found). It means no unread notification with that id belongs to that user.

Solutions

  1. Treat NotificationNotFound as idempotent success — the goal (notification read/gone) is already achieved
  2. Refresh the notification list on this error to drop stale ids
  3. Key dedupe by notificationId so parallel tabs don't both send markAsRead
  4. Catch the error by code (notification_not_found) and skip user-facing error toasts for it

Example fix

// before
await notificationApi.markAsRead(notificationId);

// after
try {
  await notificationApi.markAsRead(notificationId);
} catch (e) {
  if (e.extensions?.code !== 'notification_not_found') throw e;
  // already read or deleted — nothing to do
}
Defensive patterns

Strategy: try-catch

Validate before calling

// dedupe read-marks so parallel tabs don't race
if (markedReadLocal.has(notificationId)) return;
markedReadLocal.add(notificationId);
await markAsRead(notificationId);

Type guard

function isNotificationNotFound(e: unknown): boolean {
  return (e as { extensions?: { code?: string } }).extensions?.code === 'notification_not_found';
}

Try / catch

try {
  await notificationApi.markAsRead(id);
} catch (e) {
  if (isNotificationNotFound(e)) {
    await refreshNotifications(); // drop stale id; treat as success
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Marking read a notification that was already read-and-deleted or purged by retention; a second session/tab consuming the same notification first; stale notification ids from an outdated list; notifications deleted when their source content was removed.

Common situations: Multi-tab races where both tabs flush read-state; clients replaying queued mutations after reconnect; retention jobs cleaning old notifications while a long-lived client still lists them.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18). Data as JSON: /api/errors/cd862196fd0184d0. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/notification/service.ts:574

    this.logger.debug(
      `Invitation review declined email sent to user ${receiver.id} for workspace ${workspaceId}`
    );
  }

  private async ensureWorkspaceContentExists(workspaceId: string) {
    await this.docReader.getWorkspaceContent(workspaceId);
  }

  async markAsRead(userId: string, notificationId: string) {
    try {
      await this.models.notification.markAsRead(notificationId, userId);
    } catch (err) {
      if (
        err instanceof Prisma.PrismaClientKnownRequestError &&
        err.code === 'P2025'
      ) {
        // https://www.prisma.io/docs/orm/reference/error-reference#p2025
        throw new NotificationNotFound();
      }
      throw err;
    }
    await this.publishCountChanged(userId, 'read');
  }

  async markAllAsRead(userId: string) {
    await this.models.notification.markAllAsRead(userId);
    await this.publishCountChanged(userId, 'read-all');
  }

  /**
   * Find notifications by user id, order by createdAt desc
   */
  async findManyByUserId(userId: string, options?: PaginationInput) {
    const notifications = await this.models.notification.findManyByUserId(
      userId,
      options

View on GitHub (pinned to 2af30773ae)