actualbudget/actual · error

Failed to add user access: ${error.message}

Error message

Failed to add user access: ${error.message}

What it means

addUserAccess catches all errors from its try block and, if not a UNIQUE constraint violation, rethrows them as `Failed to add user access: <cause>`. This covers failures from the existence checks and the INSERT itself (locked db, disk error, schema problems). The original message is appended after the colon.

Source

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

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(',');

        const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Read the cause after the colon to identify the actual failure
  2. For 'database is locked', reduce concurrent writes or retry with backoff
  3. Check account.sqlite file permissions and available disk space on the server
  4. If the cause is 'User or file not found' or 'Access already exists', handle those specific cases per their own guidance

Example fix

// before
catch (e) { alert('Share failed'); }
// after
try {
  addUserAccess(userId, fileId);
} catch (e) {
  if (e.message.includes('database is locked')) {
    await retry(() => addUserAccess(userId, fileId));
  } else {
    alert(`Share failed: ${e.message}`);
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!userId || !fileId) throw new Error('userId and fileId required');
if (!getUserById(userId) || !getFileById(fileId)) {
  throw new Error('user or file does not exist');
}

Type guard

function canAttemptGrant(userId, fileId) {
  return Boolean(userId) && Boolean(fileId) &&
         getUserById(userId) !== null && getFileById(fileId) !== null;
}

Try / catch

try {
  addUserAccess(userId, fileId);
} catch (e) {
  const cause = e.message.replace('Failed to add user access: ', '');
  if (cause.includes('database is locked')) {
    await backoffRetry(() => addUserAccess(userId, fileId), 3);
  } else {
    logger.error({ cause }, 'addUserAccess failed');
    throw e;
  }
}

Prevention

When it happens

Trigger: Any non-duplicate failure inside addUserAccess: a SQLite error during INSERT (database locked, read-only file, disk full), or an unexpected error thrown by getUserById/getFileById lookups.

Common situations: 'database is locked' under concurrent sync-server writes; permission problems after moving the data directory; disk exhaustion on self-hosted servers; debugging share failures where the real cause is hidden by the wrapper text.

Related errors


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