actualbudget/actual · warning

Access already exists

Error message

Access already exists

What it means

addUserAccess inserts into user_access, which has a UNIQUE constraint on (user_id, file_id). When the INSERT violates it, SQLite raises a UNIQUE constraint error which this function detects and rethrows as 'Access already exists' — meaning the user already has access to the file. It is an idempotency signal, not corruption.

Solutions

  1. Check existing access first (getUserAccess or countUserAccess) and skip the insert if it already returns a row
  2. Treat this error as success/no-op in callers that only need the access to exist (idempotent handling)
  3. Debounce/disable the share action in the UI after the first submission
  4. Use INSERT OR IGNORE semantics at the call site pattern: check-then-insert or catch-and-continue

Example fix

// before
await addUserAccess(userId, fileId); // throws on retry
// after
const existing = countUserAccess(fileId, userId);
if (!existing) {
  await addUserAccess(userId, fileId);
}
Defensive patterns

Strategy: validation

Validate before calling

import { countUserAccess } from './services/user-service';
async function grantAccessOnce(userId, fileId) {
  if (countUserAccess(fileId, userId) > 0) return; // already shared
  await addUserAccess(userId, fileId);
}

Type guard

function hasAccess(userId, fileId) {
  return countUserAccess(fileId, userId) > 0;
}

Try / catch

try {
  addUserAccess(userId, fileId);
} catch (e) {
  if (e.message.includes('Access already exists')) {
    return; // idempotent success
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling addUserAccess twice with the same (userId, fileId) pair — double-clicking a share button, retrying a request that actually succeeded, replaying a share operation, or concurrent requests sharing the same file with the same user simultaneously.

Common situations: Front-end share forms submitting twice without disabling the button; automation scripts re-running without checking current access; race conditions between two admins sharing the same file at once; re-running a migration/import script.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/4f9ac9026fb3a50a. Report an issue: GitHub.

Appendix: source

Thrown at packages/sync-server/src/services/user-service.ts:199

}

export function addUserAccess(userId, fileId) {
  if (!userId || !fileId) {
    throw new Error('Invalid parameters');
  }
  try {
    const userExists = getUserById(userId);
    const fileExists = getFileById(fileId);
    if (!userExists || !fileExists) {
      throw new Error('User or file not found');
    }
    getAccountDb().mutate(
      'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',
      [userId, fileId],
    );
  } catch (error) {
    if (error.message.includes('UNIQUE constraint')) {
      throw new Error('Access already exists');
    }
    throw new Error(`Failed to add user access: ${error.message}`);
  }
}

export function deleteUserAccessByFileId(userIds, fileId) {
  if (!Array.isArray(userIds) || userIds.length === 0) {
    throw new Error('The provided userIds must be a non-empty array.');
  }

  const CHUNK_SIZE = 999;
  let totalChanges = 0;

  try {
    getAccountDb().transaction(() => {
      for (let i = 0; i < userIds.length; i += CHUNK_SIZE) {
        const chunk = userIds.slice(i, i + CHUNK_SIZE);
        const placeholders = chunk.map(() => '?').join(',');

View on GitHub (pinned to d4334cb6e6)