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
- 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
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
- 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
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,
optionsView on GitHub (pinned to 2af30773ae)