toeverything/AFFiNE · warning · NotificationNotFound

notification_not_found

notification_not_found

Error message

Notification not found.

What it means

NotificationNotFound (code=notification_not_found) thrown by markAsRead when Prisma raises P2025 (record not found) — meaning no notification row matched the (notificationId, userId) pair. The service catches P2025 specifically and rethrows as a typed NotFound; any other Prisma error is rethrown unchanged.

Source

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

    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 26c515e050)

Solutions

  1. Treat code=notification_not_found as idempotent success on the client (the end state — notification read/gone — is achieved) rather than an error.
  2. Prune local notification lists when a clear-all or bulk action happens to avoid stale ids.
  3. Have markAsRead return void/no-op on not-found instead of throwing if idempotency is desired.
  4. Verify the notificationId shape before submitting to catch obvious client corruption.

Example fix

// before — throws on already-deleted notification
await this.models.notification.markAsRead(notificationId, userId);

// after — treat P2025 as success (already in desired state)
async markAsRead(userId, notificationId) {
  try {
    await this.models.notification.markAsRead(notificationId, userId);
  } catch (err) {
    if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') {
      return; // notification already gone — nothing to mark
    }
    throw err;
  }
  await this.publishCountChanged(userId, 'read');
}
Defensive patterns

Strategy: try-catch

Validate before calling

function assertNotificationId(id: string) {
  if (!/^[0-9a-f-]{16,}$/i.test(id)) throw new UserError('Invalid notification id');
}
// Best prevention: treat not-found as idempotent success on the client.

Type guard

function isNotificationNotFound(e: unknown): boolean {
  return e instanceof Error && (e as any).code === 'notification_not_found';
}

Try / catch

try {
  await service.markAsRead(me.id, notificationId);
} catch (e) {
  if (isNotificationNotFound(e)) {
    // already gone — desired state achieved
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Client calls markAsRead with a notificationId that doesn't exist, belongs to another user, or was already deleted. Race with a delete/clear-all. Id from a stale push payload.

Common situations: Stale notification center UI holding ids for notifications cleared on another device. Web push with an old id after the user cleared all. Notification expired/archived between delivery and read.

Related errors


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