{"record":{"id":"cd862196fd0184d0","repo":"toeverything/AFFiNE","slug":"notification-not-found","errorCode":"notification_not_found","errorMessage":"Notification not found.","messagePattern":"Notification not found\\.","errorType":"exception","errorClass":"NotificationNotFound","httpStatus":404,"severity":"warning","filePath":"packages/backend/server/src/core/notification/service.ts","lineNumber":574,"sourceCode":"    this.logger.debug(\n      `Invitation review declined email sent to user ${receiver.id} for workspace ${workspaceId}`\n    );\n  }\n\n  private async ensureWorkspaceContentExists(workspaceId: string) {\n    await this.docReader.getWorkspaceContent(workspaceId);\n  }\n\n  async markAsRead(userId: string, notificationId: string) {\n    try {\n      await this.models.notification.markAsRead(notificationId, userId);\n    } catch (err) {\n      if (\n        err instanceof Prisma.PrismaClientKnownRequestError &&\n        err.code === 'P2025'\n      ) {\n        // https://www.prisma.io/docs/orm/reference/error-reference#p2025\n        throw new NotificationNotFound();\n      }\n      throw err;\n    }\n    await this.publishCountChanged(userId, 'read');\n  }\n\n  async markAllAsRead(userId: string) {\n    await this.models.notification.markAllAsRead(userId);\n    await this.publishCountChanged(userId, 'read-all');\n  }\n\n  /**\n   * Find notifications by user id, order by createdAt desc\n   */\n  async findManyByUserId(userId: string, options?: PaginationInput) {\n    const notifications = await this.models.notification.findManyByUserId(\n      userId,\n      options","sourceCodeStart":556,"sourceCodeEnd":592,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/notification/service.ts#L556-L592","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Treat NotificationNotFound as idempotent success — the goal (notification read/gone) is already achieved","Refresh the notification list on this error to drop stale ids","Key dedupe by notificationId so parallel tabs don't both send markAsRead","Catch the error by code (notification_not_found) and skip user-facing error toasts for it"],"exampleFix":"// before\nawait notificationApi.markAsRead(notificationId);\n\n// after\ntry {\n  await notificationApi.markAsRead(notificationId);\n} catch (e) {\n  if (e.extensions?.code !== 'notification_not_found') throw e;\n  // already read or deleted — nothing to do\n}","handlingStrategy":"try-catch","validationCode":"// dedupe read-marks so parallel tabs don't race\nif (markedReadLocal.has(notificationId)) return;\nmarkedReadLocal.add(notificationId);\nawait markAsRead(notificationId);","typeGuard":"function isNotificationNotFound(e: unknown): boolean {\n  return (e as { extensions?: { code?: string } }).extensions?.code === 'notification_not_found';\n}","tryCatchPattern":"try {\n  await notificationApi.markAsRead(id);\n} catch (e) {\n  if (isNotificationNotFound(e)) {\n    await refreshNotifications(); // drop stale id; treat as success\n    return;\n  }\n  throw e;\n}","preventionTips":["Treat mark-as-read as idempotent: swallow notification_not_found","Dedupe read-marks by notificationId across tabs/sessions","Refresh the notification list on reconnect before replaying queued mutations"],"tags":["notification","prisma","not-found","idempotency"],"backgroundTag":"record-not-found","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}